♻️ Refactored localization service
This commit is contained in:
@@ -5,7 +5,7 @@ using DysonNetwork.Shared.Proto;
|
|||||||
using DysonNetwork.Shared.Queue;
|
using DysonNetwork.Shared.Queue;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
using NATS.Client.Core;
|
using NATS.Client.Core;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Extensions;
|
using NodaTime.Extensions;
|
||||||
@@ -15,7 +15,7 @@ namespace DysonNetwork.Pass.Account;
|
|||||||
public class AccountEventService(
|
public class AccountEventService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
IStringLocalizer<Localization.AccountEventResource> localizer,
|
ILocalizationService localizer,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
Pass.Leveling.ExperienceService experienceService,
|
Pass.Leveling.ExperienceService experienceService,
|
||||||
RemotePaymentService payment,
|
RemotePaymentService payment,
|
||||||
@@ -359,8 +359,8 @@ public class AccountEventService(
|
|||||||
new CheckInFortuneTip
|
new CheckInFortuneTip
|
||||||
{
|
{
|
||||||
IsPositive = true,
|
IsPositive = true,
|
||||||
Title = localizer["FortuneTipSpecialTitle_Birthday"].Value,
|
Title = localizer.Get("fortuneTipSpecialTitleBirthday"),
|
||||||
Content = localizer["FortuneTipSpecialContent_Birthday", user.Nick].Value,
|
Content = localizer.Get("fortuneTipSpecialContentBirthday", args: new { user.Nick }),
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -374,8 +374,8 @@ public class AccountEventService(
|
|||||||
tips = positiveIndices.Select(index => new CheckInFortuneTip
|
tips = positiveIndices.Select(index => new CheckInFortuneTip
|
||||||
{
|
{
|
||||||
IsPositive = true,
|
IsPositive = true,
|
||||||
Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value,
|
Title = localizer.Get($"fortuneTipPositiveTitle{index}"),
|
||||||
Content = localizer[$"FortuneTipPositiveContent_{index}"].Value
|
Content = localizer.Get($"fortuneTipPositiveContent{index}")
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Generate 2 negative tips
|
// Generate 2 negative tips
|
||||||
@@ -387,8 +387,8 @@ public class AccountEventService(
|
|||||||
tips.AddRange(negativeIndices.Select(index => new CheckInFortuneTip
|
tips.AddRange(negativeIndices.Select(index => new CheckInFortuneTip
|
||||||
{
|
{
|
||||||
IsPositive = false,
|
IsPositive = false,
|
||||||
Title = localizer[$"FortuneTipNegativeTitle_{index}"].Value,
|
Title = localizer.Get($"fortuneTipNegativeTitle{index}"),
|
||||||
Content = localizer[$"FortuneTipNegativeContent_{index}"].Value
|
Content = localizer.Get($"fortuneTipNegativeContent{index}")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// The 5 is specialized, keep it alone.
|
// The 5 is specialized, keep it alone.
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ using DysonNetwork.Pass.Mailer;
|
|||||||
using DysonNetwork.Pass.Resources.Emails;
|
using DysonNetwork.Pass.Resources.Emails;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
|
using DysonNetwork.Shared.Localization;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Queue;
|
using DysonNetwork.Shared.Queue;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using NATS.Client.Core;
|
using NATS.Client.Core;
|
||||||
using NATS.Net;
|
using NATS.Net;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -28,8 +28,7 @@ public class AccountService(
|
|||||||
AffiliationSpellService ars,
|
AffiliationSpellService ars,
|
||||||
EmailService mailer,
|
EmailService mailer,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
IStringLocalizer<EmailResource> emailLocalizer,
|
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
ILogger<AccountService> logger,
|
ILogger<AccountService> logger,
|
||||||
RemoteSubscriptionService remoteSubscription,
|
RemoteSubscriptionService remoteSubscription,
|
||||||
@@ -434,8 +433,8 @@ public class AccountService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "auth.verification",
|
Topic = "auth.verification",
|
||||||
Title = localizer["AuthCodeTitle"],
|
Title = localizer.Get("authCodeTitle"),
|
||||||
Body = localizer["AuthCodeBody", code],
|
Body = localizer.Get("authCodeBody", args: new { code }),
|
||||||
IsSavable = false
|
IsSavable = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,7 +465,7 @@ public class AccountService(
|
|||||||
.SendTemplatedEmailAsync<FactorCodeEmail, VerificationEmailModel>(
|
.SendTemplatedEmailAsync<FactorCodeEmail, VerificationEmailModel>(
|
||||||
account.Nick,
|
account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
emailLocalizer["CodeEmailTitle"],
|
localizer.Get("codeEmailTitle"),
|
||||||
new VerificationEmailModel
|
new VerificationEmailModel
|
||||||
{
|
{
|
||||||
Name = account.Name,
|
Name = account.Name,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using DysonNetwork.Pass.Resources.Emails;
|
|||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using EmailResource = DysonNetwork.Pass.Localization.EmailResource;
|
using EmailResource = DysonNetwork.Pass.Localization.EmailResource;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ public class MagicSpellService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<MagicSpellService> logger,
|
ILogger<MagicSpellService> logger,
|
||||||
IStringLocalizer<EmailResource> localizer,
|
ILocalizationService localizer,
|
||||||
EmailService email,
|
EmailService email,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
@@ -98,7 +98,7 @@ public class MagicSpellService(
|
|||||||
await email.SendTemplatedEmailAsync<RegistrationConfirmEmail, LandingEmailModel>(
|
await email.SendTemplatedEmailAsync<RegistrationConfirmEmail, LandingEmailModel>(
|
||||||
contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
localizer["RegConfirmTitle"],
|
localizer.Get("regConfirmTitle"),
|
||||||
new LandingEmailModel
|
new LandingEmailModel
|
||||||
{
|
{
|
||||||
Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
@@ -110,7 +110,7 @@ public class MagicSpellService(
|
|||||||
await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
|
await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
|
||||||
contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
localizer["AccountDeletionTitle"],
|
localizer.Get("accountDeletionTitle"),
|
||||||
new AccountDeletionEmailModel
|
new AccountDeletionEmailModel
|
||||||
{
|
{
|
||||||
Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
@@ -122,7 +122,7 @@ public class MagicSpellService(
|
|||||||
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
||||||
contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
contact.Content,
|
contact.Content,
|
||||||
localizer["PasswordResetTitle"],
|
localizer.Get("passwordResetTitle"),
|
||||||
new PasswordResetEmailModel
|
new PasswordResetEmailModel
|
||||||
{
|
{
|
||||||
Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
@@ -136,7 +136,7 @@ public class MagicSpellService(
|
|||||||
await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
|
await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
|
||||||
contact.Account.Nick,
|
contact.Account.Nick,
|
||||||
contactMethod!,
|
contactMethod!,
|
||||||
localizer["ContractVerificationTitle"],
|
localizer.Get("contractVerificationTitle"),
|
||||||
new ContactVerificationEmailModel
|
new ContactVerificationEmailModel
|
||||||
{
|
{
|
||||||
Name = contact.Account.Name,
|
Name = contact.Account.Name,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using DysonNetwork.Shared.Cache;
|
|||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Account;
|
namespace DysonNetwork.Pass.Account;
|
||||||
@@ -12,7 +12,7 @@ public class RelationshipService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer
|
ILocalizationService localizer
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
||||||
@@ -117,8 +117,8 @@ public class RelationshipService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "relationships.friends.request",
|
Topic = "relationships.friends.request",
|
||||||
Title = localizer["FriendRequestTitle", sender.Nick],
|
Title = localizer.Get("friendRequestTitle", args: new { sender.Nick }),
|
||||||
Body = localizer["FriendRequestBody"],
|
Body = localizer.Get("friendRequestBody"),
|
||||||
ActionUri = "/account/relationships",
|
ActionUri = "/account/relationships",
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using DysonNetwork.Pass.Localization;
|
using DysonNetwork.Pass.Localization;
|
||||||
using DysonNetwork.Shared.Geometry;
|
using DysonNetwork.Shared.Geometry;
|
||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
||||||
using ActionLogService = DysonNetwork.Pass.Account.ActionLogService;
|
using ActionLogService = DysonNetwork.Pass.Account.ActionLogService;
|
||||||
using DysonNetwork.Shared.Models;
|
using DysonNetwork.Shared.Models;
|
||||||
@@ -22,7 +22,7 @@ public class AuthController(
|
|||||||
ActionLogService als,
|
ActionLogService als,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
ILogger<AuthController> logger
|
ILogger<AuthController> logger
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
@@ -237,9 +237,8 @@ public class AuthController(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "auth.login",
|
Topic = "auth.login",
|
||||||
Title = localizer["NewLoginTitle"],
|
Title = localizer.Get("newLoginTitle"),
|
||||||
Body = localizer["NewLoginBody", challenge.DeviceName ?? "unknown",
|
Body = localizer.Get("newLoginBody", args: new { deviceName = challenge.DeviceName ?? "unknown", ipAddress = challenge.IpAddress ?? "unknown" }),
|
||||||
challenge.IpAddress ?? "unknown"],
|
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
},
|
},
|
||||||
UserId = challenge.AccountId.ToString()
|
UserId = challenge.AccountId.ToString()
|
||||||
|
|||||||
@@ -134,4 +134,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Migrations\" />
|
<Folder Include="Migrations\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\Locales\*.json" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ using DysonNetwork.Shared.Models;
|
|||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Realm;
|
namespace DysonNetwork.Pass.Realm;
|
||||||
|
|
||||||
public class RealmService(
|
public class RealmService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -55,8 +55,8 @@ public class RealmService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "invites.realms",
|
Topic = "invites.realms",
|
||||||
Title = localizer["RealmInviteTitle"],
|
Title = localizer.Get("realmInviteTitle"),
|
||||||
Body = localizer["RealmInviteBody", member.Realm.Name],
|
Body = localizer.Get("realmInviteBody", args: new { realmName = member.Realm.Name }),
|
||||||
ActionUri = "/realms",
|
ActionUri = "/realms",
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using DysonNetwork.Pass.Localization;
|
using DysonNetwork.Pass.Localization;
|
||||||
using DysonNetwork.Shared;
|
using DysonNetwork.Shared;
|
||||||
using DysonNetwork.Shared.Cache;
|
using DysonNetwork.Shared.Cache;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
|
|
||||||
namespace DysonNetwork.Pass.Realm;
|
namespace DysonNetwork.Pass.Realm;
|
||||||
|
|
||||||
public class RealmServiceGrpc(
|
public class RealmServiceGrpc(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
ICacheService cache
|
ICacheService cache
|
||||||
)
|
)
|
||||||
: Shared.Proto.RealmService.RealmServiceBase
|
: Shared.Proto.RealmService.RealmServiceBase
|
||||||
@@ -127,8 +127,8 @@ public class RealmServiceGrpc(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "invites.realms",
|
Topic = "invites.realms",
|
||||||
Title = localizer["RealmInviteTitle"],
|
Title = localizer.Get("realmInviteTitle"),
|
||||||
Body = localizer["RealmInviteBody", member.Realm?.Name ?? "Unknown Realm"],
|
Body = localizer.Get("realmInviteBody", args: new { realmName = member.Realm?.Name ?? "Unknown Realm" }),
|
||||||
ActionUri = "/realms",
|
ActionUri = "/realms",
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
}
|
}
|
||||||
|
|||||||
22
DysonNetwork.Pass/Resources/Locales/en.json
Normal file
22
DysonNetwork.Pass/Resources/Locales/en.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"welcomeMessage": "Hello, {name}!",
|
||||||
|
"goodbyeMessage": "Goodbye, {name}!",
|
||||||
|
"itemCount": {
|
||||||
|
"one": "{count} item",
|
||||||
|
"other": "{count} items"
|
||||||
|
},
|
||||||
|
"userCount": {
|
||||||
|
"one": "There is {count} user online",
|
||||||
|
"other": "There are {count} users online"
|
||||||
|
},
|
||||||
|
"notificationMessage": "You have {count} new message(s) from {sender}",
|
||||||
|
"profileUpdated": "Your profile has been updated successfully",
|
||||||
|
"errorOccurred": "An error occurred: {error}",
|
||||||
|
"loginSuccess": "Welcome back, {username}!",
|
||||||
|
"loginFailed": "Login failed. Please try again.",
|
||||||
|
"accountCreated": "Your account has been created successfully",
|
||||||
|
"emailVerification": "Please verify your email address",
|
||||||
|
"passwordReset": "Password reset link has been sent to your email",
|
||||||
|
"permissionDenied": "You do not have permission to access this resource",
|
||||||
|
"lotteryWon": "Congratulations! You won the lottery!"
|
||||||
|
}
|
||||||
22
DysonNetwork.Pass/Resources/Locales/zh-hans.json
Normal file
22
DysonNetwork.Pass/Resources/Locales/zh-hans.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"welcomeMessage": "你好,{name}!",
|
||||||
|
"goodbyeMessage": "再见,{name}!",
|
||||||
|
"itemCount": {
|
||||||
|
"one": "{count}个项目",
|
||||||
|
"other": "{count}个项目"
|
||||||
|
},
|
||||||
|
"userCount": {
|
||||||
|
"one": "有 {count} 位用户在线",
|
||||||
|
"other": "有 {count} 位用户在线"
|
||||||
|
},
|
||||||
|
"notificationMessage": "您有 {count} 条来自 {sender} 的新消息",
|
||||||
|
"profileUpdated": "您的个人资料已成功更新",
|
||||||
|
"errorOccurred": "发生错误:{error}",
|
||||||
|
"loginSuccess": "欢迎回来,{username}!",
|
||||||
|
"loginFailed": "登录失败,请重试",
|
||||||
|
"accountCreated": "您的账户已成功创建",
|
||||||
|
"emailVerification": "请验证您的电子邮件地址",
|
||||||
|
"passwordReset": "密码重置链接已发送到您的邮箱",
|
||||||
|
"permissionDenied": "您没有权限访问此资源",
|
||||||
|
"lotteryWon": "恭喜!您赢得了抽奖!"
|
||||||
|
}
|
||||||
6
DysonNetwork.Shared/Localization/ILocalizationService.cs
Normal file
6
DysonNetwork.Shared/Localization/ILocalizationService.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DysonNetwork.Shared.Localization;
|
||||||
|
|
||||||
|
public interface ILocalizationService
|
||||||
|
{
|
||||||
|
string Get(string key, string? locale = null, object? args = null);
|
||||||
|
}
|
||||||
262
DysonNetwork.Shared/Localization/JsonLocalizationService.cs
Normal file
262
DysonNetwork.Shared/Localization/JsonLocalizationService.cs
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Localization;
|
||||||
|
|
||||||
|
public class JsonLocalizationService : ILocalizationService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Dictionary<string, LocalizationEntry>> _localeCache = new();
|
||||||
|
private readonly Assembly _assembly;
|
||||||
|
private readonly string _resourceNamespace;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly List<string> _availableLocales = new();
|
||||||
|
|
||||||
|
public JsonLocalizationService(Assembly? assembly = null, string? resourceNamespace = null)
|
||||||
|
{
|
||||||
|
_assembly = assembly ?? Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly();
|
||||||
|
_resourceNamespace = resourceNamespace ?? "DysonNetwork.Pass.Resources.Locales";
|
||||||
|
DiscoverAvailableLocales();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiscoverAvailableLocales()
|
||||||
|
{
|
||||||
|
var resourceNames = _assembly.GetManifestResourceNames();
|
||||||
|
var prefix = $"{_resourceNamespace}.";
|
||||||
|
var suffix = ".json";
|
||||||
|
|
||||||
|
foreach (var resourceName in resourceNames)
|
||||||
|
{
|
||||||
|
if (resourceName.StartsWith(prefix) && resourceName.EndsWith(suffix))
|
||||||
|
{
|
||||||
|
var locale = resourceName.Substring(prefix.Length, resourceName.Length - prefix.Length - suffix.Length);
|
||||||
|
_availableLocales.Add(locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Get(string key, string? locale = null, object? args = null)
|
||||||
|
{
|
||||||
|
locale ??= CultureInfo.CurrentUICulture.Name;
|
||||||
|
|
||||||
|
// Try the requested locale first
|
||||||
|
var entries = GetLocaleEntries(locale);
|
||||||
|
if (entries.TryGetValue(key, out var entry))
|
||||||
|
{
|
||||||
|
return FormatEntry(entry, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: search all available locales
|
||||||
|
foreach (var availableLocale in _availableLocales)
|
||||||
|
{
|
||||||
|
if (availableLocale.Equals(locale, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var fallbackEntries = GetLocaleEntries(availableLocale);
|
||||||
|
if (fallbackEntries.TryGetValue(key, out entry))
|
||||||
|
{
|
||||||
|
return FormatEntry(entry, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no translation found, return key with args joined
|
||||||
|
return FormatFallback(key, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatEntry(LocalizationEntry entry, object? args)
|
||||||
|
{
|
||||||
|
string template;
|
||||||
|
if (args != null && entry.IsPlural)
|
||||||
|
{
|
||||||
|
template = SelectPluralForm(entry, args);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
template = !string.IsNullOrEmpty(entry.Value) ? entry.Value : (entry.Other ?? entry.One ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormatTemplate(template, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatFallback(string key, object? args)
|
||||||
|
{
|
||||||
|
if (args == null)
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract argument values and join them with the key
|
||||||
|
var argValues = new List<string>();
|
||||||
|
var type = args.GetType();
|
||||||
|
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var value = prop.GetValue(args);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
argValues.Add(value.ToString() ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
var value = field.GetValue(args);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
argValues.Add(value.ToString() ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argValues.Count == 0)
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{key} {string.Join(" ", argValues)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, LocalizationEntry> GetLocaleEntries(string locale)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_localeCache.TryGetValue(locale, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = LoadLocale(locale);
|
||||||
|
_localeCache[locale] = entries;
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, LocalizationEntry> LoadLocale(string locale)
|
||||||
|
{
|
||||||
|
var resourceName = $"{_resourceNamespace}.{locale}.json";
|
||||||
|
|
||||||
|
using var stream = _assembly.GetManifestResourceStream(resourceName);
|
||||||
|
if (stream == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, LocalizationEntry>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
|
||||||
|
var root = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
|
||||||
|
if (root == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, LocalizationEntry>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries = new Dictionary<string, LocalizationEntry>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var kvp in root)
|
||||||
|
{
|
||||||
|
entries[kvp.Key] = ParseLocalizationEntry(kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalizationEntry ParseLocalizationEntry(JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return new LocalizationEntry { Value = element.GetString() ?? string.Empty };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
var entry = new LocalizationEntry();
|
||||||
|
if (element.TryGetProperty("value", out var valueProp))
|
||||||
|
{
|
||||||
|
entry.Value = valueProp.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
if (element.TryGetProperty("one", out var oneProp))
|
||||||
|
{
|
||||||
|
entry.One = oneProp.GetString();
|
||||||
|
}
|
||||||
|
if (element.TryGetProperty("other", out var otherProp))
|
||||||
|
{
|
||||||
|
entry.Other = otherProp.GetString();
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LocalizationEntry { Value = element.ToString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private string SelectPluralForm(LocalizationEntry entry, object args)
|
||||||
|
{
|
||||||
|
var countValue = GetCountValue(args);
|
||||||
|
if (countValue.HasValue)
|
||||||
|
{
|
||||||
|
var count = countValue.Value;
|
||||||
|
if (count == 1 && !string.IsNullOrEmpty(entry.One))
|
||||||
|
{
|
||||||
|
return entry.One;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.Other ?? entry.One ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int? GetCountValue(object args)
|
||||||
|
{
|
||||||
|
if (args == null) return null;
|
||||||
|
|
||||||
|
var type = args.GetType();
|
||||||
|
var countProperty = type.GetProperty("Count", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||||
|
if (countProperty != null)
|
||||||
|
{
|
||||||
|
var value = countProperty.GetValue(args);
|
||||||
|
if (value is int intValue) return intValue;
|
||||||
|
if (value is long longValue) return (int)longValue;
|
||||||
|
if (value is double doubleValue) return (int)doubleValue;
|
||||||
|
if (value is decimal decimalValue) return (int)decimalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var countField = type.GetField("Count", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||||
|
if (countField != null)
|
||||||
|
{
|
||||||
|
var value = countField.GetValue(args);
|
||||||
|
if (value is int intValue) return intValue;
|
||||||
|
if (value is long longValue) return (int)longValue;
|
||||||
|
if (value is double doubleValue) return (int)doubleValue;
|
||||||
|
if (value is decimal decimalValue) return (int)decimalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatTemplate(string template, object? args)
|
||||||
|
{
|
||||||
|
if (args == null || string.IsNullOrEmpty(template))
|
||||||
|
{
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = template;
|
||||||
|
var type = args.GetType();
|
||||||
|
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var placeholder = $"{{{prop.Name}}}";
|
||||||
|
var value = prop.GetValue(args);
|
||||||
|
result = result.Replace(placeholder, value?.ToString() ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
var placeholder = $"{{{field.Name}}}";
|
||||||
|
var value = field.GetValue(args);
|
||||||
|
result = result.Replace(placeholder, value?.ToString() ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
DysonNetwork.Shared/Localization/LocalizationEntry.cs
Normal file
12
DysonNetwork.Shared/Localization/LocalizationEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace DysonNetwork.Shared.Localization;
|
||||||
|
|
||||||
|
public class LocalizationEntry
|
||||||
|
{
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? One { get; set; }
|
||||||
|
|
||||||
|
public string? Other { get; set; }
|
||||||
|
|
||||||
|
public bool IsPlural => One != null || Other != null;
|
||||||
|
}
|
||||||
54
DysonNetwork.Shared/Localization/LocalizationExtensions.cs
Normal file
54
DysonNetwork.Shared/Localization/LocalizationExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace DysonNetwork.Shared.Localization;
|
||||||
|
|
||||||
|
public static class LocalizationExtensions
|
||||||
|
{
|
||||||
|
public static string Localize(this string key, string? locale = null, object? args = null)
|
||||||
|
{
|
||||||
|
var service = LocalizationServiceLocator.Service;
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return service.Get(key, locale, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string LocalizeCount(this string key, int count, string? locale = null, object? additionalArgs = null)
|
||||||
|
{
|
||||||
|
object args;
|
||||||
|
if (additionalArgs != null)
|
||||||
|
{
|
||||||
|
args = new CountWrapper(count, additionalArgs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
args = new { count };
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = LocalizationServiceLocator.Service;
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return service.Get(key, locale, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CountWrapper
|
||||||
|
{
|
||||||
|
public int Count { get; }
|
||||||
|
|
||||||
|
private readonly object? _additionalArgs;
|
||||||
|
|
||||||
|
public CountWrapper(int count, object? additionalArgs)
|
||||||
|
{
|
||||||
|
Count = count;
|
||||||
|
_additionalArgs = additionalArgs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocalizationServiceLocator
|
||||||
|
{
|
||||||
|
public static ILocalizationService? Service { get; set; }
|
||||||
|
}
|
||||||
@@ -95,4 +95,8 @@
|
|||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\Locales\*.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using DysonNetwork.Sphere.Localization;
|
|||||||
using DysonNetwork.Sphere.Publisher;
|
using DysonNetwork.Sphere.Publisher;
|
||||||
using DysonNetwork.Sphere.ActivityPub;
|
using DysonNetwork.Sphere.ActivityPub;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Markdig;
|
using Markdig;
|
||||||
using AngleSharp.Html.Parser;
|
using AngleSharp.Html.Parser;
|
||||||
@@ -19,7 +19,7 @@ namespace DysonNetwork.Sphere.Post;
|
|||||||
|
|
||||||
public partial class PostService(
|
public partial class PostService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
IServiceScopeFactory factory,
|
IServiceScopeFactory factory,
|
||||||
FlushBufferService flushBuffer,
|
FlushBufferService flushBuffer,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
@@ -124,9 +124,9 @@ public partial class PostService(
|
|||||||
? string.Concat(post.Content.AsSpan(0, 97), "...")
|
? string.Concat(post.Content.AsSpan(0, 97), "...")
|
||||||
: post.Content;
|
: post.Content;
|
||||||
var title = post.Title ?? (post.Content?.Length >= 10 ? post.Content[..10] + "..." : post.Content);
|
var title = post.Title ?? (post.Content?.Length >= 10 ? post.Content[..10] + "..." : post.Content);
|
||||||
title ??= localizer["PostOnlyMedia"];
|
title ??= localizer.Get("postOnlyMedia");
|
||||||
if (string.IsNullOrWhiteSpace(content))
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
content = localizer["PostOnlyMedia"];
|
content = localizer.Get("postOnlyMedia");
|
||||||
return (title, content);
|
return (title, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ public partial class PostService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "post.replies",
|
Topic = "post.replies",
|
||||||
Title = localizer["PostReplyTitle", sender!.Nick],
|
Title = localizer.Get("postReplyTitle", args: new { senderNick = sender!.Nick }),
|
||||||
Body = ChopPostForNotification(post).content,
|
Body = ChopPostForNotification(post).content,
|
||||||
IsSavable = true,
|
IsSavable = true,
|
||||||
ActionUri = $"/posts/{post.Id}"
|
ActionUri = $"/posts/{post.Id}"
|
||||||
@@ -703,11 +703,10 @@ public partial class PostService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "posts.reactions.new",
|
Topic = "posts.reactions.new",
|
||||||
Title = localizer["PostReactTitle", sender.Nick],
|
Title = localizer.Get("postReactTitle", args: new { senderNick = sender.Nick }),
|
||||||
Body = string.IsNullOrWhiteSpace(post.Title)
|
Body = string.IsNullOrWhiteSpace(post.Title)
|
||||||
? localizer["PostReactBody", sender.Nick, reaction.Symbol]
|
? localizer.Get("postReactBody", args: new { senderNick = sender.Nick, reaction.Symbol })
|
||||||
: localizer["PostReactContentBody", sender.Nick, reaction.Symbol,
|
: localizer.Get("postReactContentBody", args: new { senderNick = sender.Nick, reaction.Symbol, post.Title }),
|
||||||
post.Title],
|
|
||||||
IsSavable = true,
|
IsSavable = true,
|
||||||
ActionUri = $"/posts/{post.Id}"
|
ActionUri = $"/posts/{post.Id}"
|
||||||
}
|
}
|
||||||
@@ -1157,11 +1156,10 @@ public partial class PostService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "posts.awards.new",
|
Topic = "posts.awards.new",
|
||||||
Title = localizer["PostAwardedTitle", sender.Nick],
|
Title = localizer.Get("postAwardedTitle", args: new { senderNick = sender.Nick }),
|
||||||
Body = string.IsNullOrWhiteSpace(post.Title)
|
Body = string.IsNullOrWhiteSpace(post.Title)
|
||||||
? localizer["PostAwardedBody", sender.Nick, amount]
|
? localizer.Get("postAwardedBody", args: new { senderNick = sender.Nick, amount })
|
||||||
: localizer["PostAwardedContentBody", sender.Nick, amount,
|
: localizer.Get("postAwardedContentBody", args: new { senderNick = sender.Nick, amount, post.Title }),
|
||||||
post.Title],
|
|
||||||
IsSavable = true,
|
IsSavable = true,
|
||||||
ActionUri = $"/posts/{post.Id}"
|
ActionUri = $"/posts/{post.Id}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ using DysonNetwork.Shared.Models;
|
|||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Sphere.Localization;
|
using DysonNetwork.Sphere.Localization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
|
|
||||||
namespace DysonNetwork.Sphere.Publisher;
|
namespace DysonNetwork.Sphere.Publisher;
|
||||||
|
|
||||||
public class PublisherSubscriptionService(
|
public class PublisherSubscriptionService(
|
||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
Post.PostService ps,
|
Post.PostService ps,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
AccountService.AccountServiceClient accounts
|
AccountService.AccountServiceClient accounts
|
||||||
@@ -118,7 +118,7 @@ public class PublisherSubscriptionService(
|
|||||||
var notification = new PushNotification
|
var notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "posts.new",
|
Topic = "posts.new",
|
||||||
Title = localizer["PostSubscriptionTitle", post.Publisher!.Nick, title],
|
Title = localizer.Get("postSubscriptionTitle", args: new { publisherNick = post.Publisher!.Nick, title }),
|
||||||
Body = message,
|
Body = message,
|
||||||
Meta = GrpcTypeHelper.ConvertObjectToByteString(data),
|
Meta = GrpcTypeHelper.ConvertObjectToByteString(data),
|
||||||
IsSavable = true,
|
IsSavable = true,
|
||||||
|
|||||||
13
DysonNetwork.Sphere/Resources/Locales/en.json
Normal file
13
DysonNetwork.Sphere/Resources/Locales/en.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"postOnlyMedia": "Media only",
|
||||||
|
"postReplyTitle": "{user} replied to your post",
|
||||||
|
"postReactTitle": "{user} reacted to your post",
|
||||||
|
"postReactBody": "{user} reacted with {reaction}",
|
||||||
|
"postReactContentBody": "{user} reacted with {reaction} to \"{title}\"",
|
||||||
|
"postAwardedTitle": "{user} awarded your post",
|
||||||
|
"postAwardedBody": "{user} awarded {amount} points",
|
||||||
|
"postAwardedContentBody": "{user} awarded {amount} points to \"{title}\"",
|
||||||
|
"postSubscriptionTitle": "{publisher}: {title}",
|
||||||
|
"realmInviteTitle": "Realm Invitation",
|
||||||
|
"realmInviteBody": "You have been invited to join {realm}"
|
||||||
|
}
|
||||||
13
DysonNetwork.Sphere/Resources/Locales/zh-hans.json
Normal file
13
DysonNetwork.Sphere/Resources/Locales/zh-hans.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"postOnlyMedia": "仅媒体",
|
||||||
|
"postReplyTitle": "{user} 回复了您的帖子",
|
||||||
|
"postReactTitle": "{user} 对您的帖子做出了反应",
|
||||||
|
"postReactBody": "{user} 用 {reaction} 做出了反应",
|
||||||
|
"postReactContentBody": "{user} 用 {reaction} 对 \"{title}\" 做出了反应",
|
||||||
|
"postAwardedTitle": "{user} 打赏了您的帖子",
|
||||||
|
"postAwardedBody": "{user} 打赏了 {amount} 积分",
|
||||||
|
"postAwardedContentBody": "{user} 打赏了 {amount} 积分给 \"{title}\"",
|
||||||
|
"postSubscriptionTitle": "{publisher}: {title}",
|
||||||
|
"realmInviteTitle": "领域邀请",
|
||||||
|
"realmInviteBody": "您被邀请加入 {realm}"
|
||||||
|
}
|
||||||
@@ -29,4 +29,8 @@
|
|||||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\Locales\*.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using DysonNetwork.Shared.Proto;
|
|||||||
using DysonNetwork.Shared.Queue;
|
using DysonNetwork.Shared.Queue;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
using NATS.Client.Core;
|
using NATS.Client.Core;
|
||||||
using NATS.Net;
|
using NATS.Net;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
@@ -17,7 +17,7 @@ public class PaymentService(
|
|||||||
AppDatabase db,
|
AppDatabase db,
|
||||||
WalletService wat,
|
WalletService wat,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
INatsConnection nats
|
INatsConnection nats
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -189,14 +189,16 @@ public class PaymentService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "wallets.transactions",
|
Topic = "wallets.transactions",
|
||||||
Title = localizer["TransactionNewTitle", readableTransactionRemark],
|
Title = localizer.Get("transactionNewTitle", args: new { remark = readableTransactionRemark }),
|
||||||
Body = transaction.Amount > 0
|
Body = transaction.Amount > 0
|
||||||
? localizer["TransactionNewBodyMinus",
|
? localizer.Get("transactionNewBodyMinus", args: new {
|
||||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
transaction.Currency]
|
currency = transaction.Currency
|
||||||
: localizer["TransactionNewBodyPlus",
|
})
|
||||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
: localizer.Get("transactionNewBodyPlus", args: new {
|
||||||
transaction.Currency],
|
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
currency = transaction.Currency
|
||||||
|
}),
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,14 +218,16 @@ public class PaymentService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "wallets.transactions",
|
Topic = "wallets.transactions",
|
||||||
Title = localizer["TransactionNewTitle", readableTransactionRemark],
|
Title = localizer.Get("transactionNewTitle", args: new { remark = readableTransactionRemark }),
|
||||||
Body = transaction.Amount > 0
|
Body = transaction.Amount > 0
|
||||||
? localizer["TransactionNewBodyPlus",
|
? localizer.Get("transactionNewBodyPlus", args: new {
|
||||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
transaction.Currency]
|
currency = transaction.Currency
|
||||||
: localizer["TransactionNewBodyMinus",
|
})
|
||||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
: localizer.Get("transactionNewBodyMinus", args: new {
|
||||||
transaction.Currency],
|
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
|
currency = transaction.Currency
|
||||||
|
}),
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,10 +324,12 @@ public class PaymentService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "wallets.orders.paid",
|
Topic = "wallets.orders.paid",
|
||||||
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
Title = localizer.Get("orderPaidTitle", args: new { orderId = $"#{readableOrderId}" }),
|
||||||
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture),
|
Body = localizer.Get("orderPaidBody", args: new {
|
||||||
order.Currency,
|
amount = order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
readableOrderRemark],
|
currency = order.Currency,
|
||||||
|
remark = readableOrderRemark
|
||||||
|
}),
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,10 +349,12 @@ public class PaymentService(
|
|||||||
Notification = new PushNotification
|
Notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "wallets.orders.received",
|
Topic = "wallets.orders.received",
|
||||||
Title = localizer["OrderReceivedTitle", $"#{readableOrderId}"],
|
Title = localizer.Get("orderReceivedTitle", args: new { orderId = $"#{readableOrderId}" }),
|
||||||
Body = localizer["OrderReceivedBody", order.Amount.ToString(CultureInfo.InvariantCulture),
|
Body = localizer.Get("orderReceivedBody", args: new {
|
||||||
order.Currency,
|
amount = order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||||
readableOrderRemark],
|
currency = order.Currency,
|
||||||
|
remark = readableOrderRemark
|
||||||
|
}),
|
||||||
IsSavable = true
|
IsSavable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using DysonNetwork.Shared.Models;
|
|||||||
using DysonNetwork.Shared.Proto;
|
using DysonNetwork.Shared.Proto;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Localization;
|
using DysonNetwork.Shared.Localization;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using Duration = NodaTime.Duration;
|
using Duration = NodaTime.Duration;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ public class SubscriptionService(
|
|||||||
PaymentService payment,
|
PaymentService payment,
|
||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
RingService.RingServiceClient pusher,
|
RingService.RingServiceClient pusher,
|
||||||
IStringLocalizer<NotificationResource> localizer,
|
ILocalizationService localizer,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ICacheService cache,
|
ICacheService cache,
|
||||||
ILogger<SubscriptionService> logger
|
ILogger<SubscriptionService> logger
|
||||||
@@ -423,8 +423,8 @@ public class SubscriptionService(
|
|||||||
var notification = new PushNotification
|
var notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "subscriptions.begun",
|
Topic = "subscriptions.begun",
|
||||||
Title = localizer["SubscriptionAppliedTitle", humanReadableName],
|
Title = localizer.Get("subscriptionAppliedTitle", args: new { subscriptionName = humanReadableName }),
|
||||||
Body = localizer["SubscriptionAppliedBody", duration, humanReadableName],
|
Body = localizer.Get("subscriptionAppliedBody", args: new { duration, subscriptionName = humanReadableName }),
|
||||||
Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object>
|
Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["subscription_id"] = subscription.Id.ToString()
|
["subscription_id"] = subscription.Id.ToString()
|
||||||
@@ -920,8 +920,8 @@ public class SubscriptionService(
|
|||||||
var notification = new PushNotification
|
var notification = new PushNotification
|
||||||
{
|
{
|
||||||
Topic = "gifts.claimed",
|
Topic = "gifts.claimed",
|
||||||
Title = localizer["GiftClaimedTitle"],
|
Title = localizer.Get("giftClaimedTitle"),
|
||||||
Body = localizer["GiftClaimedBody", humanReadableName, redeemer.Name],
|
Body = localizer.Get("giftClaimedBody", args: new { subscriptionName = humanReadableName, redeemerName = redeemer.Name }),
|
||||||
Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object>
|
Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["gift_id"] = gift.Id.ToString(),
|
["gift_id"] = gift.Id.ToString(),
|
||||||
|
|||||||
13
DysonNetwork.Wallet/Resources/Locales/en.json
Normal file
13
DysonNetwork.Wallet/Resources/Locales/en.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"transactionNewTitle": "New Transaction",
|
||||||
|
"transactionNewBodyMinus": "You spent {amount} {currency}",
|
||||||
|
"transactionNewBodyPlus": "You received {amount} {currency}",
|
||||||
|
"orderPaidTitle": "Order Paid",
|
||||||
|
"orderPaidBody": "Paid {amount} {currency} for {remark}",
|
||||||
|
"orderReceivedTitle": "Order Received",
|
||||||
|
"orderReceivedBody": "Received {amount} {currency} for {remark}",
|
||||||
|
"subscriptionAppliedTitle": "Subscription Applied",
|
||||||
|
"subscriptionAppliedBody": "Your subscription for {duration} days of {subscription} has been applied",
|
||||||
|
"giftClaimedTitle": "Gift Claimed",
|
||||||
|
"giftClaimedBody": "Your gift of {subscription} has been claimed by {user}"
|
||||||
|
}
|
||||||
13
DysonNetwork.Wallet/Resources/Locales/zh-hans.json
Normal file
13
DysonNetwork.Wallet/Resources/Locales/zh-hans.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"transactionNewTitle": "新交易",
|
||||||
|
"transactionNewBodyMinus": "您花费了 {amount} {currency}",
|
||||||
|
"transactionNewBodyPlus": "您收到了 {amount} {currency}",
|
||||||
|
"orderPaidTitle": "订单已支付",
|
||||||
|
"orderPaidBody": "支付了 {amount} {currency} 用于 {remark}",
|
||||||
|
"orderReceivedTitle": "订单已接收",
|
||||||
|
"orderReceivedBody": "收到了 {amount} {currency} 用于 {remark}",
|
||||||
|
"subscriptionAppliedTitle": "订阅已应用",
|
||||||
|
"subscriptionAppliedBody": "您的 {duration} 天 {subscription} 订阅已应用",
|
||||||
|
"giftClaimedTitle": "礼物已领取",
|
||||||
|
"giftClaimedBody": "您的 {subscription} 礼物已被 {user} 领取"
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"Debug": true,
|
|
||||||
"BaseUrl": "http://localhost:5071",
|
|
||||||
"SiteUrl": "https://solian.app",
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"App": "Host=host.docker.internal;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
|
||||||
},
|
|
||||||
"KnownProxies": ["127.0.0.1", "::1"]
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
{
|
|
||||||
"Debug": true,
|
|
||||||
"BaseUrl": "http://localhost:5090",
|
|
||||||
"GatewayUrl": "http://localhost:5094",
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"App": "Host=host.docker.internal;Port=5432;Database=dyson_drive;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=5;Connection Idle Lifetime=30"
|
|
||||||
},
|
|
||||||
"Authentication": {
|
|
||||||
"Schemes": {
|
|
||||||
"Bearer": {
|
|
||||||
"ValidAudiences": ["http://localhost:5071", "https://localhost:7099"],
|
|
||||||
"ValidIssuer": "solar-network"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AuthToken": {
|
|
||||||
"PublicKeyPath": "app/keys/PublicKey.pem",
|
|
||||||
"PrivateKeyPath": "app/keys/PrivateKey.pem"
|
|
||||||
},
|
|
||||||
"OidcProvider": {
|
|
||||||
"IssuerUri": "https://nt.solian.app",
|
|
||||||
"PublicKeyPath": "Keys/PublicKey.pem",
|
|
||||||
"PrivateKeyPath": "Keys/PrivateKey.pem",
|
|
||||||
"AccessTokenLifetime": "01:00:00",
|
|
||||||
"RefreshTokenLifetime": "30.00:00:00",
|
|
||||||
"AuthorizationCodeLifetime": "00:30:00",
|
|
||||||
"RequireHttpsMetadata": true
|
|
||||||
},
|
|
||||||
"Tus": {
|
|
||||||
"StorePath": "/app/uploads"
|
|
||||||
},
|
|
||||||
"Storage": {
|
|
||||||
"PreferredRemote": "2adceae3-981a-4564-9b8d-5d71a211c873",
|
|
||||||
"Remote": [
|
|
||||||
{
|
|
||||||
"Id": "minio",
|
|
||||||
"Label": "Minio",
|
|
||||||
"Region": "auto",
|
|
||||||
"Bucket": "solar-network-development",
|
|
||||||
"Endpoint": "minio.orb.local:9000",
|
|
||||||
"SecretId": "littlesheep",
|
|
||||||
"SecretKey": "password",
|
|
||||||
"EnabledSigned": true,
|
|
||||||
"EnableSsl": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id": "cloudflare",
|
|
||||||
"Label": "Cloudflare R2",
|
|
||||||
"Region": "auto",
|
|
||||||
"Bucket": "solar-network",
|
|
||||||
"Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com",
|
|
||||||
"SecretId": "8ff5d06c7b1639829d60bc6838a542e6",
|
|
||||||
"SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67",
|
|
||||||
"EnableSigned": true,
|
|
||||||
"EnableSsl": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Captcha": {
|
|
||||||
"Provider": "cloudflare",
|
|
||||||
"ApiKey": "0x4AAAAAABCDUdOujj4feOb_",
|
|
||||||
"ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U"
|
|
||||||
},
|
|
||||||
"Notifications": {
|
|
||||||
"Topic": "dev.solsynth.solian",
|
|
||||||
"Endpoint": "http://localhost:8088"
|
|
||||||
},
|
|
||||||
"Email": {
|
|
||||||
"Server": "smtp4dev.orb.local",
|
|
||||||
"Port": 25,
|
|
||||||
"UseSsl": false,
|
|
||||||
"Username": "no-reply@mail.solsynth.dev",
|
|
||||||
"Password": "password",
|
|
||||||
"FromAddress": "no-reply@mail.solsynth.dev",
|
|
||||||
"FromName": "Alphabot",
|
|
||||||
"SubjectPrefix": "Solar Network"
|
|
||||||
},
|
|
||||||
"RealtimeChat": {
|
|
||||||
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
|
||||||
"ApiKey": "APIs6TiL8wj3A4j",
|
|
||||||
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
|
|
||||||
},
|
|
||||||
"GeoIp": {
|
|
||||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
|
||||||
},
|
|
||||||
"Oidc": {
|
|
||||||
"Google": {
|
|
||||||
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
|
|
||||||
"ClientSecret": ""
|
|
||||||
},
|
|
||||||
"Apple": {
|
|
||||||
"ClientId": "dev.solsynth.solian",
|
|
||||||
"TeamId": "W7HPZ53V6B",
|
|
||||||
"KeyId": "B668YP4KBG",
|
|
||||||
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
|
||||||
},
|
|
||||||
"Microsoft": {
|
|
||||||
"ClientId": "YOUR_MICROSOFT_CLIENT_ID",
|
|
||||||
"ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET",
|
|
||||||
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Payment": {
|
|
||||||
"Auth": {
|
|
||||||
"Afdian": "<token here>"
|
|
||||||
},
|
|
||||||
"Subscriptions": {
|
|
||||||
"Afdian": {
|
|
||||||
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
|
|
||||||
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
|
|
||||||
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"KnownProxies": ["127.0.0.1", "::1"]
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"Debug": true,
|
|
||||||
"BaseUrl": "http://localhost:5001",
|
|
||||||
"SiteUrl": "http://localhost:3000",
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"App": "Host=host.docker.internal;Port=5432;Database=dyson_pass;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
|
||||||
},
|
|
||||||
"Authentication": {
|
|
||||||
"Schemes": {
|
|
||||||
"Bearer": {
|
|
||||||
"ValidAudiences": ["http://localhost:5071", "https://localhost:7099"],
|
|
||||||
"ValidIssuer": "solar-network"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AuthToken": {
|
|
||||||
"CookieDomain": "localhost",
|
|
||||||
"PublicKeyPath": "/app/keys/PublicKey.pem",
|
|
||||||
"PrivateKeyPath": "/app/keys/PrivateKey.pem"
|
|
||||||
},
|
|
||||||
"OidcProvider": {
|
|
||||||
"IssuerUri": "https://nt.solian.app",
|
|
||||||
"PublicKeyPath": "Keys/PublicKey.pem",
|
|
||||||
"PrivateKeyPath": "Keys/PrivateKey.pem",
|
|
||||||
"AccessTokenLifetime": "01:00:00",
|
|
||||||
"RefreshTokenLifetime": "30.00:00:00",
|
|
||||||
"AuthorizationCodeLifetime": "00:30:00",
|
|
||||||
"RequireHttpsMetadata": true
|
|
||||||
},
|
|
||||||
"Captcha": {
|
|
||||||
"Provider": "cloudflare",
|
|
||||||
"ApiKey": "0x4AAAAAABCDUdOujj4feOb_",
|
|
||||||
"ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U"
|
|
||||||
},
|
|
||||||
"GeoIp": {
|
|
||||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
|
||||||
},
|
|
||||||
"Oidc": {
|
|
||||||
"Google": {
|
|
||||||
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
|
|
||||||
"ClientSecret": ""
|
|
||||||
},
|
|
||||||
"Apple": {
|
|
||||||
"ClientId": "dev.solsynth.solian",
|
|
||||||
"TeamId": "W7HPZ53V6B",
|
|
||||||
"KeyId": "B668YP4KBG",
|
|
||||||
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
|
||||||
},
|
|
||||||
"Microsoft": {
|
|
||||||
"ClientId": "YOUR_MICROSOFT_CLIENT_ID",
|
|
||||||
"ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET",
|
|
||||||
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Payment": {
|
|
||||||
"Auth": {
|
|
||||||
"Afdian": "<token here>"
|
|
||||||
},
|
|
||||||
"Subscriptions": {
|
|
||||||
"Afdian": {
|
|
||||||
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
|
|
||||||
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
|
|
||||||
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"KnownProxies": ["127.0.0.1", "::1"]
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"Debug": true,
|
|
||||||
"BaseUrl": "http://localhost:5212",
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"App": "Host=host.docker.internal;Port=5432;Database=dyson_ring;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
|
||||||
},
|
|
||||||
"Notifications": {
|
|
||||||
"Push": {
|
|
||||||
"Production": true,
|
|
||||||
"Google": "/app/keys/Solian.json",
|
|
||||||
"Apple": {
|
|
||||||
"PrivateKey": "./Keys/Solian.p8",
|
|
||||||
"PrivateKeyId": "4US4KSX4W6",
|
|
||||||
"TeamId": "W7HPZ53V6B",
|
|
||||||
"BundleIdentifier": "dev.solsynth.solian"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Email": {
|
|
||||||
"Server": "smtp4dev.orb.local",
|
|
||||||
"Port": 25,
|
|
||||||
"UseSsl": false,
|
|
||||||
"Username": "no-reply@mail.solsynth.dev",
|
|
||||||
"Password": "password",
|
|
||||||
"FromAddress": "no-reply@mail.solsynth.dev",
|
|
||||||
"FromName": "Alphabot",
|
|
||||||
"SubjectPrefix": "Solar Network"
|
|
||||||
},
|
|
||||||
"GeoIp": {
|
|
||||||
"DatabasePath": "/app/keys/GeoLite2-City.mmdb"
|
|
||||||
},
|
|
||||||
"KnownProxies": ["127.0.0.1", "::1"]
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"Debug": true,
|
|
||||||
"BaseUrl": "http://localhost:5071",
|
|
||||||
"SiteUrl": "https://solian.app",
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"App": "Host=host.docker.internal;Port=5432;Database=dyson_sphere;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60"
|
|
||||||
},
|
|
||||||
"GeoIp": {
|
|
||||||
"DatabasePath": "/app/keys/GeoLite2-City.mmdb"
|
|
||||||
},
|
|
||||||
"RealtimeChat": {
|
|
||||||
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
|
||||||
"ApiKey": "",
|
|
||||||
"ApiSecret": ""
|
|
||||||
},
|
|
||||||
"Translation": {
|
|
||||||
"Provider": "Tencent",
|
|
||||||
"Region": "ap-hongkong",
|
|
||||||
"ProjectId": "0",
|
|
||||||
"SecretId": "",
|
|
||||||
"SecretKey": ""
|
|
||||||
},
|
|
||||||
"KnownProxies": ["127.0.0.1", "::1"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user