♻️ Refactored localization service
This commit is contained in:
@@ -5,7 +5,7 @@ using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Queue;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using NATS.Client.Core;
|
||||
using NodaTime;
|
||||
using NodaTime.Extensions;
|
||||
@@ -15,7 +15,7 @@ namespace DysonNetwork.Pass.Account;
|
||||
public class AccountEventService(
|
||||
AppDatabase db,
|
||||
ICacheService cache,
|
||||
IStringLocalizer<Localization.AccountEventResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
RingService.RingServiceClient pusher,
|
||||
Pass.Leveling.ExperienceService experienceService,
|
||||
RemotePaymentService payment,
|
||||
@@ -359,8 +359,8 @@ public class AccountEventService(
|
||||
new CheckInFortuneTip
|
||||
{
|
||||
IsPositive = true,
|
||||
Title = localizer["FortuneTipSpecialTitle_Birthday"].Value,
|
||||
Content = localizer["FortuneTipSpecialContent_Birthday", user.Nick].Value,
|
||||
Title = localizer.Get("fortuneTipSpecialTitleBirthday"),
|
||||
Content = localizer.Get("fortuneTipSpecialContentBirthday", args: new { user.Nick }),
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -374,8 +374,8 @@ public class AccountEventService(
|
||||
tips = positiveIndices.Select(index => new CheckInFortuneTip
|
||||
{
|
||||
IsPositive = true,
|
||||
Title = localizer[$"FortuneTipPositiveTitle_{index}"].Value,
|
||||
Content = localizer[$"FortuneTipPositiveContent_{index}"].Value
|
||||
Title = localizer.Get($"fortuneTipPositiveTitle{index}"),
|
||||
Content = localizer.Get($"fortuneTipPositiveContent{index}")
|
||||
}).ToList();
|
||||
|
||||
// Generate 2 negative tips
|
||||
@@ -387,8 +387,8 @@ public class AccountEventService(
|
||||
tips.AddRange(negativeIndices.Select(index => new CheckInFortuneTip
|
||||
{
|
||||
IsPositive = false,
|
||||
Title = localizer[$"FortuneTipNegativeTitle_{index}"].Value,
|
||||
Content = localizer[$"FortuneTipNegativeContent_{index}"].Value
|
||||
Title = localizer.Get($"fortuneTipNegativeTitle{index}"),
|
||||
Content = localizer.Get($"fortuneTipNegativeContent{index}")
|
||||
}));
|
||||
|
||||
// The 5 is specialized, keep it alone.
|
||||
|
||||
@@ -6,11 +6,11 @@ using DysonNetwork.Pass.Mailer;
|
||||
using DysonNetwork.Pass.Resources.Emails;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Queue;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Net;
|
||||
using NodaTime;
|
||||
@@ -28,8 +28,7 @@ public class AccountService(
|
||||
AffiliationSpellService ars,
|
||||
EmailService mailer,
|
||||
RingService.RingServiceClient pusher,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
IStringLocalizer<EmailResource> emailLocalizer,
|
||||
ILocalizationService localizer,
|
||||
ICacheService cache,
|
||||
ILogger<AccountService> logger,
|
||||
RemoteSubscriptionService remoteSubscription,
|
||||
@@ -434,8 +433,8 @@ public class AccountService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "auth.verification",
|
||||
Title = localizer["AuthCodeTitle"],
|
||||
Body = localizer["AuthCodeBody", code],
|
||||
Title = localizer.Get("authCodeTitle"),
|
||||
Body = localizer.Get("authCodeBody", args: new { code }),
|
||||
IsSavable = false
|
||||
}
|
||||
}
|
||||
@@ -466,7 +465,7 @@ public class AccountService(
|
||||
.SendTemplatedEmailAsync<FactorCodeEmail, VerificationEmailModel>(
|
||||
account.Nick,
|
||||
contact.Content,
|
||||
emailLocalizer["CodeEmailTitle"],
|
||||
localizer.Get("codeEmailTitle"),
|
||||
new VerificationEmailModel
|
||||
{
|
||||
Name = account.Name,
|
||||
|
||||
@@ -5,7 +5,7 @@ using DysonNetwork.Pass.Resources.Emails;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using NodaTime;
|
||||
using EmailResource = DysonNetwork.Pass.Localization.EmailResource;
|
||||
|
||||
@@ -15,7 +15,7 @@ public class MagicSpellService(
|
||||
AppDatabase db,
|
||||
IConfiguration configuration,
|
||||
ILogger<MagicSpellService> logger,
|
||||
IStringLocalizer<EmailResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
EmailService email,
|
||||
ICacheService cache
|
||||
)
|
||||
@@ -98,7 +98,7 @@ public class MagicSpellService(
|
||||
await email.SendTemplatedEmailAsync<RegistrationConfirmEmail, LandingEmailModel>(
|
||||
contact.Account.Nick,
|
||||
contact.Content,
|
||||
localizer["RegConfirmTitle"],
|
||||
localizer.Get("regConfirmTitle"),
|
||||
new LandingEmailModel
|
||||
{
|
||||
Name = contact.Account.Name,
|
||||
@@ -110,7 +110,7 @@ public class MagicSpellService(
|
||||
await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
|
||||
contact.Account.Nick,
|
||||
contact.Content,
|
||||
localizer["AccountDeletionTitle"],
|
||||
localizer.Get("accountDeletionTitle"),
|
||||
new AccountDeletionEmailModel
|
||||
{
|
||||
Name = contact.Account.Name,
|
||||
@@ -122,7 +122,7 @@ public class MagicSpellService(
|
||||
await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
|
||||
contact.Account.Nick,
|
||||
contact.Content,
|
||||
localizer["PasswordResetTitle"],
|
||||
localizer.Get("passwordResetTitle"),
|
||||
new PasswordResetEmailModel
|
||||
{
|
||||
Name = contact.Account.Name,
|
||||
@@ -136,7 +136,7 @@ public class MagicSpellService(
|
||||
await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
|
||||
contact.Account.Nick,
|
||||
contactMethod!,
|
||||
localizer["ContractVerificationTitle"],
|
||||
localizer.Get("contractVerificationTitle"),
|
||||
new ContactVerificationEmailModel
|
||||
{
|
||||
Name = contact.Account.Name,
|
||||
|
||||
@@ -3,7 +3,7 @@ using DysonNetwork.Shared.Cache;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Pass.Account;
|
||||
@@ -12,7 +12,7 @@ public class RelationshipService(
|
||||
AppDatabase db,
|
||||
ICacheService cache,
|
||||
RingService.RingServiceClient pusher,
|
||||
IStringLocalizer<NotificationResource> localizer
|
||||
ILocalizationService localizer
|
||||
)
|
||||
{
|
||||
private const string UserFriendsCacheKeyPrefix = "accounts:friends:";
|
||||
@@ -117,8 +117,8 @@ public class RelationshipService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "relationships.friends.request",
|
||||
Title = localizer["FriendRequestTitle", sender.Nick],
|
||||
Body = localizer["FriendRequestBody"],
|
||||
Title = localizer.Get("friendRequestTitle", args: new { sender.Nick }),
|
||||
Body = localizer.Get("friendRequestBody"),
|
||||
ActionUri = "/account/relationships",
|
||||
IsSavable = true
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Pass.Localization;
|
||||
using DysonNetwork.Shared.Geometry;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using AccountService = DysonNetwork.Pass.Account.AccountService;
|
||||
using ActionLogService = DysonNetwork.Pass.Account.ActionLogService;
|
||||
using DysonNetwork.Shared.Models;
|
||||
@@ -22,7 +22,7 @@ public class AuthController(
|
||||
ActionLogService als,
|
||||
RingService.RingServiceClient pusher,
|
||||
IConfiguration configuration,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
ILogger<AuthController> logger
|
||||
) : ControllerBase
|
||||
{
|
||||
@@ -237,9 +237,8 @@ public class AuthController(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "auth.login",
|
||||
Title = localizer["NewLoginTitle"],
|
||||
Body = localizer["NewLoginBody", challenge.DeviceName ?? "unknown",
|
||||
challenge.IpAddress ?? "unknown"],
|
||||
Title = localizer.Get("newLoginTitle"),
|
||||
Body = localizer.Get("newLoginBody", args: new { deviceName = challenge.DeviceName ?? "unknown", ipAddress = challenge.IpAddress ?? "unknown" }),
|
||||
IsSavable = true
|
||||
},
|
||||
UserId = challenge.AccountId.ToString()
|
||||
|
||||
@@ -134,4 +134,8 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Locales\*.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,14 +5,14 @@ using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
|
||||
namespace DysonNetwork.Pass.Realm;
|
||||
|
||||
public class RealmService(
|
||||
AppDatabase db,
|
||||
RingService.RingServiceClient pusher,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
ICacheService cache
|
||||
)
|
||||
{
|
||||
@@ -55,8 +55,8 @@ public class RealmService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "invites.realms",
|
||||
Title = localizer["RealmInviteTitle"],
|
||||
Body = localizer["RealmInviteBody", member.Realm.Name],
|
||||
Title = localizer.Get("realmInviteTitle"),
|
||||
Body = localizer.Get("realmInviteBody", args: new { realmName = member.Realm.Name }),
|
||||
ActionUri = "/realms",
|
||||
IsSavable = true
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Pass.Localization;
|
||||
using DysonNetwork.Shared;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
|
||||
namespace DysonNetwork.Pass.Realm;
|
||||
|
||||
public class RealmServiceGrpc(
|
||||
AppDatabase db,
|
||||
RingService.RingServiceClient pusher,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
ICacheService cache
|
||||
)
|
||||
: Shared.Proto.RealmService.RealmServiceBase
|
||||
@@ -127,8 +127,8 @@ public class RealmServiceGrpc(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "invites.realms",
|
||||
Title = localizer["RealmInviteTitle"],
|
||||
Body = localizer["RealmInviteBody", member.Realm?.Name ?? "Unknown Realm"],
|
||||
Title = localizer.Get("realmInviteTitle"),
|
||||
Body = localizer.Get("realmInviteBody", args: new { realmName = member.Realm?.Name ?? "Unknown Realm" }),
|
||||
ActionUri = "/realms",
|
||||
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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Locales\*.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,7 +7,7 @@ using DysonNetwork.Sphere.Localization;
|
||||
using DysonNetwork.Sphere.Publisher;
|
||||
using DysonNetwork.Sphere.ActivityPub;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using NodaTime;
|
||||
using Markdig;
|
||||
using AngleSharp.Html.Parser;
|
||||
@@ -19,7 +19,7 @@ namespace DysonNetwork.Sphere.Post;
|
||||
|
||||
public partial class PostService(
|
||||
AppDatabase db,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
IServiceScopeFactory factory,
|
||||
FlushBufferService flushBuffer,
|
||||
ICacheService cache,
|
||||
@@ -124,9 +124,9 @@ public partial class PostService(
|
||||
? string.Concat(post.Content.AsSpan(0, 97), "...")
|
||||
: 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))
|
||||
content = localizer["PostOnlyMedia"];
|
||||
content = localizer.Get("postOnlyMedia");
|
||||
return (title, content);
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ public partial class PostService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "post.replies",
|
||||
Title = localizer["PostReplyTitle", sender!.Nick],
|
||||
Title = localizer.Get("postReplyTitle", args: new { senderNick = sender!.Nick }),
|
||||
Body = ChopPostForNotification(post).content,
|
||||
IsSavable = true,
|
||||
ActionUri = $"/posts/{post.Id}"
|
||||
@@ -703,11 +703,10 @@ public partial class PostService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "posts.reactions.new",
|
||||
Title = localizer["PostReactTitle", sender.Nick],
|
||||
Title = localizer.Get("postReactTitle", args: new { senderNick = sender.Nick }),
|
||||
Body = string.IsNullOrWhiteSpace(post.Title)
|
||||
? localizer["PostReactBody", sender.Nick, reaction.Symbol]
|
||||
: localizer["PostReactContentBody", sender.Nick, reaction.Symbol,
|
||||
post.Title],
|
||||
? localizer.Get("postReactBody", args: new { senderNick = sender.Nick, reaction.Symbol })
|
||||
: localizer.Get("postReactContentBody", args: new { senderNick = sender.Nick, reaction.Symbol, post.Title }),
|
||||
IsSavable = true,
|
||||
ActionUri = $"/posts/{post.Id}"
|
||||
}
|
||||
@@ -1157,11 +1156,10 @@ public partial class PostService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "posts.awards.new",
|
||||
Title = localizer["PostAwardedTitle", sender.Nick],
|
||||
Title = localizer.Get("postAwardedTitle", args: new { senderNick = sender.Nick }),
|
||||
Body = string.IsNullOrWhiteSpace(post.Title)
|
||||
? localizer["PostAwardedBody", sender.Nick, amount]
|
||||
: localizer["PostAwardedContentBody", sender.Nick, amount,
|
||||
post.Title],
|
||||
? localizer.Get("postAwardedBody", args: new { senderNick = sender.Nick, amount })
|
||||
: localizer.Get("postAwardedContentBody", args: new { senderNick = sender.Nick, amount, post.Title }),
|
||||
IsSavable = true,
|
||||
ActionUri = $"/posts/{post.Id}"
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Sphere.Localization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
|
||||
namespace DysonNetwork.Sphere.Publisher;
|
||||
|
||||
public class PublisherSubscriptionService(
|
||||
AppDatabase db,
|
||||
Post.PostService ps,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
ICacheService cache,
|
||||
RingService.RingServiceClient pusher,
|
||||
AccountService.AccountServiceClient accounts
|
||||
@@ -118,7 +118,7 @@ public class PublisherSubscriptionService(
|
||||
var notification = new PushNotification
|
||||
{
|
||||
Topic = "posts.new",
|
||||
Title = localizer["PostSubscriptionTitle", post.Publisher!.Nick, title],
|
||||
Title = localizer.Get("postSubscriptionTitle", args: new { publisherNick = post.Publisher!.Nick, title }),
|
||||
Body = message,
|
||||
Meta = GrpcTypeHelper.ConvertObjectToByteString(data),
|
||||
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}"
|
||||
}
|
||||
@@ -49,4 +49,4 @@ public class TranslationController(ITranslationProvider provider, ICacheService
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,8 @@
|
||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Locales\*.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,7 +6,7 @@ using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Queue;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using NATS.Client.Core;
|
||||
using NATS.Net;
|
||||
using NodaTime;
|
||||
@@ -17,7 +17,7 @@ public class PaymentService(
|
||||
AppDatabase db,
|
||||
WalletService wat,
|
||||
RingService.RingServiceClient pusher,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
INatsConnection nats
|
||||
)
|
||||
{
|
||||
@@ -189,14 +189,16 @@ public class PaymentService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "wallets.transactions",
|
||||
Title = localizer["TransactionNewTitle", readableTransactionRemark],
|
||||
Title = localizer.Get("transactionNewTitle", args: new { remark = readableTransactionRemark }),
|
||||
Body = transaction.Amount > 0
|
||||
? localizer["TransactionNewBodyMinus",
|
||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
transaction.Currency]
|
||||
: localizer["TransactionNewBodyPlus",
|
||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
transaction.Currency],
|
||||
? localizer.Get("transactionNewBodyMinus", args: new {
|
||||
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
currency = transaction.Currency
|
||||
})
|
||||
: localizer.Get("transactionNewBodyPlus", args: new {
|
||||
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
currency = transaction.Currency
|
||||
}),
|
||||
IsSavable = true
|
||||
}
|
||||
}
|
||||
@@ -216,14 +218,16 @@ public class PaymentService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "wallets.transactions",
|
||||
Title = localizer["TransactionNewTitle", readableTransactionRemark],
|
||||
Title = localizer.Get("transactionNewTitle", args: new { remark = readableTransactionRemark }),
|
||||
Body = transaction.Amount > 0
|
||||
? localizer["TransactionNewBodyPlus",
|
||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
transaction.Currency]
|
||||
: localizer["TransactionNewBodyMinus",
|
||||
transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
transaction.Currency],
|
||||
? localizer.Get("transactionNewBodyPlus", args: new {
|
||||
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
currency = transaction.Currency
|
||||
})
|
||||
: localizer.Get("transactionNewBodyMinus", args: new {
|
||||
amount = transaction.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
currency = transaction.Currency
|
||||
}),
|
||||
IsSavable = true
|
||||
}
|
||||
}
|
||||
@@ -320,10 +324,12 @@ public class PaymentService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "wallets.orders.paid",
|
||||
Title = localizer["OrderPaidTitle", $"#{readableOrderId}"],
|
||||
Body = localizer["OrderPaidBody", order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
order.Currency,
|
||||
readableOrderRemark],
|
||||
Title = localizer.Get("orderPaidTitle", args: new { orderId = $"#{readableOrderId}" }),
|
||||
Body = localizer.Get("orderPaidBody", args: new {
|
||||
amount = order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
currency = order.Currency,
|
||||
remark = readableOrderRemark
|
||||
}),
|
||||
IsSavable = true
|
||||
}
|
||||
}
|
||||
@@ -343,10 +349,12 @@ public class PaymentService(
|
||||
Notification = new PushNotification
|
||||
{
|
||||
Topic = "wallets.orders.received",
|
||||
Title = localizer["OrderReceivedTitle", $"#{readableOrderId}"],
|
||||
Body = localizer["OrderReceivedBody", order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
order.Currency,
|
||||
readableOrderRemark],
|
||||
Title = localizer.Get("orderReceivedTitle", args: new { orderId = $"#{readableOrderId}" }),
|
||||
Body = localizer.Get("orderReceivedBody", args: new {
|
||||
amount = order.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
currency = order.Currency,
|
||||
remark = readableOrderRemark
|
||||
}),
|
||||
IsSavable = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using DysonNetwork.Shared.Localization;
|
||||
using NodaTime;
|
||||
using Duration = NodaTime.Duration;
|
||||
|
||||
@@ -19,7 +19,7 @@ public class SubscriptionService(
|
||||
PaymentService payment,
|
||||
AccountService.AccountServiceClient accounts,
|
||||
RingService.RingServiceClient pusher,
|
||||
IStringLocalizer<NotificationResource> localizer,
|
||||
ILocalizationService localizer,
|
||||
IConfiguration configuration,
|
||||
ICacheService cache,
|
||||
ILogger<SubscriptionService> logger
|
||||
@@ -423,8 +423,8 @@ public class SubscriptionService(
|
||||
var notification = new PushNotification
|
||||
{
|
||||
Topic = "subscriptions.begun",
|
||||
Title = localizer["SubscriptionAppliedTitle", humanReadableName],
|
||||
Body = localizer["SubscriptionAppliedBody", duration, humanReadableName],
|
||||
Title = localizer.Get("subscriptionAppliedTitle", args: new { subscriptionName = humanReadableName }),
|
||||
Body = localizer.Get("subscriptionAppliedBody", args: new { duration, subscriptionName = humanReadableName }),
|
||||
Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object>
|
||||
{
|
||||
["subscription_id"] = subscription.Id.ToString()
|
||||
@@ -920,8 +920,8 @@ public class SubscriptionService(
|
||||
var notification = new PushNotification
|
||||
{
|
||||
Topic = "gifts.claimed",
|
||||
Title = localizer["GiftClaimedTitle"],
|
||||
Body = localizer["GiftClaimedBody", humanReadableName, redeemer.Name],
|
||||
Title = localizer.Get("giftClaimedTitle"),
|
||||
Body = localizer.Get("giftClaimedBody", args: new { subscriptionName = humanReadableName, redeemerName = redeemer.Name }),
|
||||
Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object>
|
||||
{
|
||||
["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