diff --git a/.editorconfig b/.editorconfig
index 8bf951e..701b6b7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -20,9 +20,9 @@ trim_trailing_whitespace = true
[*.cs]
# Organize usings
-dotnet_separate_import_directive_groups = false:suggestion
-dotnet_sort_system_directives_first = false:suggestion
-csharp_using_directive_placement = outside_namespace:suggestion
+dotnet_separate_import_directive_groups = false:warning
+dotnet_sort_system_directives_first = false:warning
+csharp_using_directive_placement = outside_namespace:warning
file_header_template = unset
# this. and Me. preferences
@@ -45,30 +45,30 @@ dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:sil
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
-dotnet_style_namespace_match_folder = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_object_initializer = true:suggestion
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_explicit_tuple_names = true:warning
+dotnet_style_namespace_match_folder = true:warning
+dotnet_style_null_propagation = true:warning
+dotnet_style_object_initializer = true:warning
dotnet_style_operator_placement_when_wrapping = beginning_of_line
-dotnet_style_prefer_auto_properties = true:suggestion
-dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
-dotnet_style_prefer_compound_assignment = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_collection_expression = when_types_loosely_match:warning
+dotnet_style_prefer_compound_assignment = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:warning
dotnet_style_prefer_conditional_expression_over_return = true:silent
-dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
-dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
-dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
-dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:warning
+dotnet_style_prefer_simplified_interpolation = true:warning
# Field preferences
dotnet_style_readonly_field = true:warning
# Parameter preferences
-dotnet_code_quality_unused_parameters = all:suggestion
+dotnet_code_quality_unused_parameters = all:warning
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
@@ -76,9 +76,9 @@ dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
# var preferences
-csharp_style_var_elsewhere = true:suggestion
-csharp_style_var_for_built_in_types = true:suggestion
-csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:warning
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
@@ -91,44 +91,44 @@ csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_prefer_extended_property_pattern = true:suggestion
-csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_prefer_extended_property_pattern = true:warning
+csharp_style_prefer_not_pattern = true:warning
csharp_style_prefer_pattern_matching = true:silent
-csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_prefer_switch_expression = true:warning
# Null-checking preferences
-csharp_style_conditional_delegate_call = true:suggestion
+csharp_style_conditional_delegate_call = true:warning
# Modifier preferences
-csharp_prefer_static_anonymous_function = true:suggestion
+csharp_prefer_static_anonymous_function = true:warning
csharp_prefer_static_local_function = true:warning
-csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async:suggestion
-csharp_style_prefer_readonly_struct = true:suggestion
-csharp_style_prefer_readonly_struct_member = true:suggestion
+csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async:warning
+csharp_style_prefer_readonly_struct = true:warning
+csharp_style_prefer_readonly_struct_member = true:warning
# Code-block preferences
-csharp_prefer_braces = true:suggestion
-csharp_prefer_simple_using_statement = true:suggestion
-csharp_style_namespace_declarations = file_scoped:suggestion
+csharp_prefer_braces = true:warning
+csharp_prefer_simple_using_statement = true:warning
+csharp_style_namespace_declarations = file_scoped:warning
csharp_style_prefer_method_group_conversion = true:silent
-csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_primary_constructors = true:warning
csharp_style_prefer_top_level_statements = true:silent
# Expression-level preferences
-csharp_prefer_simple_default_expression = true:suggestion
-csharp_style_deconstructed_variable_declaration = true:suggestion
-csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_prefer_index_operator = true:suggestion
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_null_check_over_type_check = true:suggestion
-csharp_style_prefer_range_operator = true:suggestion
-csharp_style_prefer_tuple_swap = true:suggestion
-csharp_style_prefer_utf8_string_literals = true:suggestion
-csharp_style_throw_expression = true:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_prefer_simple_default_expression = true:warning
+csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
+csharp_style_inlined_variable_declaration = true:warning
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_local_over_anonymous_function = true:warning
+csharp_style_prefer_null_check_over_type_check = true:warning
+csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_tuple_swap = true:warning
+csharp_style_prefer_utf8_string_literals = true:warning
+csharp_style_throw_expression = true:warning
+csharp_style_unused_value_assignment_preference = discard_variable:warning
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
#### C# Formatting Rules ####
@@ -182,79 +182,79 @@ csharp_preserve_single_line_statements = false
# Naming rules
-dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = warning
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
+dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = warning
dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
-dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = warning
dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
-dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.methods_should_be_pascalcase.severity = warning
dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.properties_should_be_pascalcase.severity = warning
dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.events_should_be_pascalcase.severity = warning
dotnet_naming_rule.events_should_be_pascalcase.symbols = events
dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_variables_should_be_camelcase.severity = warning
dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
-dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_constants_should_be_camelcase.severity = warning
dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
-dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.parameters_should_be_camelcase.severity = warning
dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
-dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_fields_should_be_pascalcase.severity = warning
dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
+dotnet_naming_rule.private_fields_should_be__camelcase.severity = warning
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
-dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = warning
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = _camelcase
-dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = warning
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = warning
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = warning
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = warning
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.enums_should_be_pascalcase.severity = warning
dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascalcase.severity = warning
dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
-dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = warning
dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7643b91..af61d2f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,6 +8,7 @@
+
diff --git a/src/RustPlusBot.Features.Alarms/AlarmServiceCollectionExtensions.cs b/src/RustPlusBot.Features.Alarms/AlarmServiceCollectionExtensions.cs
index cf1afe2..5cc6806 100644
--- a/src/RustPlusBot.Features.Alarms/AlarmServiceCollectionExtensions.cs
+++ b/src/RustPlusBot.Features.Alarms/AlarmServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@
using RustPlusBot.Features.Alarms.Posting;
using RustPlusBot.Features.Alarms.Relaying;
using RustPlusBot.Features.Alarms.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Alarms;
@@ -18,7 +19,7 @@ public static IServiceCollection AddAlarms(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
- services.AddSingleton(new AlarmLocalizer(AlarmLocalizationCatalog.Default));
+ services.AddRustPlusBotLocalization();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/RustPlusBot.Features.Alarms/Relaying/AlarmStateRelay.cs b/src/RustPlusBot.Features.Alarms/Relaying/AlarmStateRelay.cs
index 1581cb9..7b11def 100644
--- a/src/RustPlusBot.Features.Alarms/Relaying/AlarmStateRelay.cs
+++ b/src/RustPlusBot.Features.Alarms/Relaying/AlarmStateRelay.cs
@@ -7,6 +7,7 @@
using RustPlusBot.Features.Alarms.Rendering;
using RustPlusBot.Features.Connections.Listening;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Alarms;
using RustPlusBot.Persistence.Connections;
using RustPlusBot.Persistence.Workspace;
@@ -36,7 +37,7 @@ internal sealed partial class AlarmStateRelay(
IServiceScopeFactory scopeFactory,
IAlarmRefresher refresher,
AlarmRelayChannels channels,
- IAlarmLocalizer localizer,
+ ILocalizer localizer,
IClock clock,
ILogger logger)
{
diff --git a/src/RustPlusBot.Features.Alarms/Rendering/AlarmEmbedRenderer.cs b/src/RustPlusBot.Features.Alarms/Rendering/AlarmEmbedRenderer.cs
index 539202a..09790cf 100644
--- a/src/RustPlusBot.Features.Alarms/Rendering/AlarmEmbedRenderer.cs
+++ b/src/RustPlusBot.Features.Alarms/Rendering/AlarmEmbedRenderer.cs
@@ -2,13 +2,14 @@
using Discord;
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Domain.Alarms;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Alarms.Rendering;
/// Renders a Smart Alarm as a Discord embed + control row, and the pairing-prompt embed + row. Pure.
/// The alarm localizer.
/// The clock used to compute relative trigger times.
-internal sealed class AlarmEmbedRenderer(IAlarmLocalizer localizer, IClock clock)
+internal sealed class AlarmEmbedRenderer(ILocalizer localizer, IClock clock)
{
/// Renders the alarm embed and its control buttons.
/// The alarm to render.
@@ -20,6 +21,7 @@ internal sealed class AlarmEmbedRenderer(IAlarmLocalizer localizer, IClock clock
ArgumentNullException.ThrowIfNull(alarm);
string statusKey;
+#pragma warning disable IDE0045 // Collapsing to a nested ternary trips RCS1238/S3358 (nested conditional); the if/else chain is intentional.
if (unreachable)
{
statusKey = "alarm.status.unreachable";
@@ -32,6 +34,7 @@ internal sealed class AlarmEmbedRenderer(IAlarmLocalizer localizer, IClock clock
{
statusKey = "alarm.status.armed";
}
+#pragma warning restore IDE0045
var triggered = alarm.LastTriggeredUtc is { } t
? localizer.Get("alarm.embed.lasttriggered", culture, CompactDuration(clock.UtcNow - t))
diff --git a/src/RustPlusBot.Features.Alarms/Rendering/AlarmLocalizationCatalog.cs b/src/RustPlusBot.Features.Alarms/Rendering/AlarmLocalizationCatalog.cs
deleted file mode 100644
index d13abaf..0000000
--- a/src/RustPlusBot.Features.Alarms/Rendering/AlarmLocalizationCatalog.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-namespace RustPlusBot.Features.Alarms.Rendering;
-
-///
-/// The in-memory string catalog for Smart Alarms: culture → (key → value).
-/// English is the fallback. Intended to be passed to the per-slice
-/// constructor.
-///
-internal static class AlarmLocalizationCatalog
-{
- ///
- /// The built-in EN/FR catalog keyed by BCP-47 primary language tag.
- /// Shape: culture → key → localized string.
- ///
- public static IReadOnlyDictionary> Default { get; } =
- new Dictionary>(StringComparer.Ordinal)
- {
- ["en"] = new Dictionary(StringComparer.Ordinal)
- {
- ["alarm.status.armed"] = "🔔 Armed",
- ["alarm.status.active"] = "🚨 Active",
- ["alarm.status.unreachable"] = "⚠️ Unreachable",
- ["alarm.button.ping.on"] = "Ping @everyone: on",
- ["alarm.button.ping.off"] = "Ping @everyone: off",
- ["alarm.button.relay.on"] = "Relay to team chat: on",
- ["alarm.button.relay.off"] = "Relay to team chat: off",
- ["alarm.button.rename"] = "Rename",
- ["alarm.embed.footer"] = "Entity {0}",
- ["alarm.embed.nevertriggered"] = "Never triggered",
- ["alarm.embed.lasttriggered"] = "Last triggered {0} ago",
- ["alarm.prompt.title"] = "New alarm detected",
- ["alarm.prompt.body"] = "Detected a new Smart Alarm ({0}). Add it?",
- ["alarm.prompt.accept"] = "Accept",
- ["alarm.prompt.dismiss"] = "Dismiss",
- ["alarm.rename.modal.title"] = "Rename alarm",
- ["alarm.rename.input.label"] = "Alarm name",
- ["alarm.triggered.teamchat"] = "🚨 {0} triggered",
- },
- ["fr"] = new Dictionary(StringComparer.Ordinal)
- {
- ["alarm.status.armed"] = "🔔 Armée",
- ["alarm.status.active"] = "🚨 Active",
- ["alarm.status.unreachable"] = "⚠️ Injoignable",
- ["alarm.button.ping.on"] = "Ping @everyone : activé",
- ["alarm.button.ping.off"] = "Ping @everyone : désactivé",
- ["alarm.button.relay.on"] = "Relais tchat équipe : activé",
- ["alarm.button.relay.off"] = "Relais tchat équipe : désactivé",
- ["alarm.button.rename"] = "Renommer",
- ["alarm.embed.footer"] = "Entité {0}",
- ["alarm.embed.nevertriggered"] = "Jamais déclenchée",
- ["alarm.embed.lasttriggered"] = "Déclenchée il y a {0}",
- ["alarm.prompt.title"] = "Nouvelle alarme détectée",
- ["alarm.prompt.body"] = "Nouvelle alarme connectée détectée ({0}). L'ajouter ?",
- ["alarm.prompt.accept"] = "Accepter",
- ["alarm.prompt.dismiss"] = "Ignorer",
- ["alarm.rename.modal.title"] = "Renommer l'alarme",
- ["alarm.rename.input.label"] = "Nom de l'alarme",
- ["alarm.triggered.teamchat"] = "🚨 {0} déclenchée",
- },
- };
-}
diff --git a/src/RustPlusBot.Features.Alarms/Rendering/AlarmLocalizer.cs b/src/RustPlusBot.Features.Alarms/Rendering/AlarmLocalizer.cs
deleted file mode 100644
index 3840c58..0000000
--- a/src/RustPlusBot.Features.Alarms/Rendering/AlarmLocalizer.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Alarms.Rendering;
-
-/// Smart-alarm localizer backed by the shared .
-/// The culture → (key → value) catalog.
-internal sealed class AlarmLocalizer(IReadOnlyDictionary> catalog)
- : DictionaryLocalizer(catalog), IAlarmLocalizer;
diff --git a/src/RustPlusBot.Features.Alarms/Rendering/IAlarmLocalizer.cs b/src/RustPlusBot.Features.Alarms/Rendering/IAlarmLocalizer.cs
deleted file mode 100644
index e7b41ec..0000000
--- a/src/RustPlusBot.Features.Alarms/Rendering/IAlarmLocalizer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Alarms.Rendering;
-
-/// Resolves localized smart-alarm strings by key and culture, falling back to English.
-internal interface IAlarmLocalizer : ILocalizer;
diff --git a/src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs b/src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs
index 207eb83..fd24d92 100644
--- a/src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs
+++ b/src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs
@@ -5,8 +5,8 @@
using RustPlusBot.Features.Commands.Help;
using RustPlusBot.Features.Commands.Hosting;
using RustPlusBot.Features.Commands.Leader;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Commands.Servers;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands;
@@ -22,9 +22,7 @@ public static IServiceCollection AddCommands(this IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
- // CommandLocalizationCatalog has a required member, so register the prebuilt Default instance.
- services.AddSingleton(CommandLocalizationCatalog.Default);
- services.AddSingleton();
+ services.AddRustPlusBotLocalization();
services.AddScoped();
services.AddScoped();
diff --git a/src/RustPlusBot.Features.Commands/Handlers/AfkCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/AfkCommandHandler.cs
index 2fcd2cc..8d0e179 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/AfkCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/AfkCommandHandler.cs
@@ -1,14 +1,14 @@
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Connections.Listening;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !afk — lists team members who have been still (online + alive) past the AFK threshold.
/// The live AFK state.
/// The reply localizer.
-internal sealed class AfkCommandHandler(IAfkState afk, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class AfkCommandHandler(IAfkState afk, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "afk";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/AliveCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/AliveCommandHandler.cs
index bc3324a..c953702 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/AliveCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/AliveCommandHandler.cs
@@ -2,7 +2,7 @@
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -10,7 +10,7 @@ namespace RustPlusBot.Features.Commands.Handlers;
/// The live server query.
/// The reply localizer.
/// The clock used to compute survival durations.
-internal sealed class AliveCommandHandler(IRustServerQuery query, ICommandLocalizer localizer, IClock clock)
+internal sealed class AliveCommandHandler(IRustServerQuery query, ILocalizer localizer, IClock clock)
: ICommandHandler
{
///
diff --git a/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs
index a0770f6..6ca4d35 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/CargoCommandHandler.cs
@@ -1,10 +1,8 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
-using RustPlusBot.Features.Events.Formatting;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -12,7 +10,7 @@ namespace RustPlusBot.Features.Commands.Handlers;
/// The live event state.
/// The reply localizer.
/// For "how long ago".
-internal sealed class CargoCommandHandler(IEventState state, ICommandLocalizer localizer, IClock clock)
+internal sealed class CargoCommandHandler(IEventState state, ILocalizer localizer, IClock clock)
: ICommandHandler
{
///
@@ -22,15 +20,7 @@ internal sealed class CargoCommandHandler(IEventState state, ICommandLocalizer l
public Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
- var markers = state.GetActiveMarkers(context.GuildId, context.ServerId, MarkerKind.CargoShip);
- if (markers.Count == 0)
- {
- return Task.FromResult(localizer.Get("command.cargo.none", context.Culture));
- }
-
- var m = markers[0];
- var grid = GridReference.From(m.X, m.Y, m.Dimensions);
- var ago = DurationFormat.Compact(clock.UtcNow - m.SeenAtUtc);
- return Task.FromResult(localizer.Get("command.cargo.ok", context.Culture, grid, ago));
+ return Task.FromResult(
+ MarkerReply.For(state, context, MarkerKind.CargoShip, "command.cargo", localizer, clock));
}
}
diff --git a/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs
index b08af67..69999c3 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/ChinookCommandHandler.cs
@@ -1,10 +1,8 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
-using RustPlusBot.Features.Events.Formatting;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -12,7 +10,7 @@ namespace RustPlusBot.Features.Commands.Handlers;
/// The live event state.
/// The reply localizer.
/// For "how long ago".
-internal sealed class ChinookCommandHandler(IEventState state, ICommandLocalizer localizer, IClock clock)
+internal sealed class ChinookCommandHandler(IEventState state, ILocalizer localizer, IClock clock)
: ICommandHandler
{
///
@@ -22,15 +20,7 @@ internal sealed class ChinookCommandHandler(IEventState state, ICommandLocalizer
public Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
- var markers = state.GetActiveMarkers(context.GuildId, context.ServerId, MarkerKind.Chinook);
- if (markers.Count == 0)
- {
- return Task.FromResult(localizer.Get("command.chinook.none", context.Culture));
- }
-
- var m = markers[0];
- var grid = GridReference.From(m.X, m.Y, m.Dimensions);
- var ago = DurationFormat.Compact(clock.UtcNow - m.SeenAtUtc);
- return Task.FromResult(localizer.Get("command.chinook.ok", context.Culture, grid, ago));
+ return Task.FromResult(
+ MarkerReply.For(state, context, MarkerKind.Chinook, "command.chinook", localizer, clock));
}
}
diff --git a/src/RustPlusBot.Features.Commands/Handlers/EventsCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/EventsCommandHandler.cs
index 2777cc9..63657a4 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/EventsCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/EventsCommandHandler.cs
@@ -1,15 +1,15 @@
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Events.Classifying;
using RustPlusBot.Features.Events.Formatting;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !events — lists the most recent live events.
/// The live event state.
/// The reply localizer.
-internal sealed class EventsCommandHandler(IEventState state, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class EventsCommandHandler(IEventState state, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "events";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs
index 1f5e61d..a12f1c1 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/HeliCommandHandler.cs
@@ -1,10 +1,8 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
-using RustPlusBot.Features.Events.Formatting;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -12,7 +10,7 @@ namespace RustPlusBot.Features.Commands.Handlers;
/// The live event state.
/// The reply localizer.
/// For "how long ago".
-internal sealed class HeliCommandHandler(IEventState state, ICommandLocalizer localizer, IClock clock)
+internal sealed class HeliCommandHandler(IEventState state, ILocalizer localizer, IClock clock)
: ICommandHandler
{
///
@@ -22,15 +20,7 @@ internal sealed class HeliCommandHandler(IEventState state, ICommandLocalizer lo
public Task ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
- var markers = state.GetActiveMarkers(context.GuildId, context.ServerId, MarkerKind.PatrolHelicopter);
- if (markers.Count == 0)
- {
- return Task.FromResult(localizer.Get("command.heli.none", context.Culture));
- }
-
- var m = markers[0];
- var grid = GridReference.From(m.X, m.Y, m.Dimensions);
- var ago = DurationFormat.Compact(clock.UtcNow - m.SeenAtUtc);
- return Task.FromResult(localizer.Get("command.heli.ok", context.Culture, grid, ago));
+ return Task.FromResult(
+ MarkerReply.For(state, context, MarkerKind.PatrolHelicopter, "command.heli", localizer, clock));
}
}
diff --git a/src/RustPlusBot.Features.Commands/Handlers/LargeCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/LargeCommandHandler.cs
index 14b67da..433792b 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/LargeCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/LargeCommandHandler.cs
@@ -1,14 +1,14 @@
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !large — reports the large oil rig's status.
/// The rig state reader.
/// The reply localizer.
-internal sealed class LargeCommandHandler(IRigState rigState, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class LargeCommandHandler(IRigState rigState, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "large";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs b/src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs
new file mode 100644
index 0000000..5e40b8f
--- /dev/null
+++ b/src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs
@@ -0,0 +1,41 @@
+using RustPlusBot.Abstractions.Connections;
+using RustPlusBot.Abstractions.Time;
+using RustPlusBot.Features.Commands.Dispatching;
+using RustPlusBot.Features.Commands.Formatting;
+using RustPlusBot.Features.Events.Formatting;
+using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
+
+namespace RustPlusBot.Features.Commands.Handlers;
+
+/// Shared map-marker reply formatting for the !cargo/!heli/!chinook handlers.
+internal static class MarkerReply
+{
+ /// Formats the localized reply for the most recent active marker of a kind.
+ /// The live event state reader.
+ /// The command context.
+ /// Which marker kind to report.
+ /// The localization key prefix ("command.cargo" / "command.heli" / "command.chinook").
+ /// The reply localizer.
+ /// For the "how long ago" suffix.
+ /// The localized reply.
+ public static string For(
+ IEventState state,
+ CommandContext context,
+ MarkerKind kind,
+ string prefix,
+ ILocalizer localizer,
+ IClock clock)
+ {
+ var markers = state.GetActiveMarkers(context.GuildId, context.ServerId, kind);
+ if (markers.Count == 0)
+ {
+ return localizer.Get($"{prefix}.none", context.Culture);
+ }
+
+ var m = markers[0];
+ var grid = GridReference.From(m.X, m.Y, m.Dimensions);
+ var ago = DurationFormat.Compact(clock.UtcNow - m.SeenAtUtc);
+ return localizer.Get($"{prefix}.ok", context.Culture, grid, ago);
+ }
+}
diff --git a/src/RustPlusBot.Features.Commands/Handlers/MuteCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/MuteCommandHandler.cs
index cc1d5e3..7e4d671 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/MuteCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/MuteCommandHandler.cs
@@ -1,5 +1,5 @@
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Commands;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -7,7 +7,7 @@ namespace RustPlusBot.Features.Commands.Handlers;
/// !mute — silence all bot-to-game output.
/// The mute store.
/// The reply localizer.
-internal sealed class MuteCommandHandler(IMuteStore store, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class MuteCommandHandler(IMuteStore store, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "mute";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/OfflineCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/OfflineCommandHandler.cs
index 7d360b6..3abd423 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/OfflineCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/OfflineCommandHandler.cs
@@ -1,13 +1,13 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !offline — lists teammates not currently connected.
/// The live server query.
/// The reply localizer.
-internal sealed class OfflineCommandHandler(IRustServerQuery query, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class OfflineCommandHandler(IRustServerQuery query, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "offline";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/OnlineCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/OnlineCommandHandler.cs
index 9dc61ad..9ddc5b6 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/OnlineCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/OnlineCommandHandler.cs
@@ -1,13 +1,13 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !online — lists currently-connected teammates.
/// The live server query.
/// The reply localizer.
-internal sealed class OnlineCommandHandler(IRustServerQuery query, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class OnlineCommandHandler(IRustServerQuery query, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "online";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/PopCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/PopCommandHandler.cs
index eda2e4b..0d4e9bd 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/PopCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/PopCommandHandler.cs
@@ -1,13 +1,13 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !pop — reports current population, slot cap and queue.
/// The live server query.
/// The reply localizer.
-internal sealed class PopCommandHandler(IRustServerQuery query, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class PopCommandHandler(IRustServerQuery query, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "pop";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/ProxCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/ProxCommandHandler.cs
index f7ce298..da6e3ce 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/ProxCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/ProxCommandHandler.cs
@@ -1,14 +1,14 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !prox [name] — distance from the caller to each other teammate (optionally one).
/// The live server query.
/// The reply localizer.
-internal sealed class ProxCommandHandler(IRustServerQuery query, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class ProxCommandHandler(IRustServerQuery query, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "prox";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/RigReply.cs b/src/RustPlusBot.Features.Commands/Handlers/RigReply.cs
index 40b9d18..e83c929 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/RigReply.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/RigReply.cs
@@ -1,8 +1,8 @@
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -21,7 +21,7 @@ public static string For(
CommandContext context,
RigKind rig,
string prefix,
- ICommandLocalizer localizer)
+ ILocalizer localizer)
{
var state = rigState.Get(context.GuildId, context.ServerId, rig);
return state.Status switch
diff --git a/src/RustPlusBot.Features.Commands/Handlers/SmallCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/SmallCommandHandler.cs
index d1f83a7..442b500 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/SmallCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/SmallCommandHandler.cs
@@ -1,14 +1,14 @@
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !small — reports the small oil rig's status.
/// The rig state reader.
/// The reply localizer.
-internal sealed class SmallCommandHandler(IRigState rigState, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class SmallCommandHandler(IRigState rigState, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "small";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/SteamIdCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/SteamIdCommandHandler.cs
index 1c7a009..11f64a5 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/SteamIdCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/SteamIdCommandHandler.cs
@@ -2,14 +2,14 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !steamid [name] — reports teammates' Steam ids (all, or filtered by partial name).
/// The live server query.
/// The reply localizer.
-internal sealed class SteamIdCommandHandler(IRustServerQuery query, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class SteamIdCommandHandler(IRustServerQuery query, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "steamid";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/TeamCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/TeamCommandHandler.cs
index c9fa0f5..66135ce 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/TeamCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/TeamCommandHandler.cs
@@ -1,13 +1,13 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !team — lists every team member's name.
/// The live server query.
/// The reply localizer.
-internal sealed class TeamCommandHandler(IRustServerQuery query, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class TeamCommandHandler(IRustServerQuery query, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "team";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/TimeCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/TimeCommandHandler.cs
index 62ba061..0b0361e 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/TimeCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/TimeCommandHandler.cs
@@ -1,14 +1,14 @@
using System.Globalization;
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !time — reports the in-game clock and whether it is day or night.
/// The live server query.
/// The reply localizer.
-internal sealed class TimeCommandHandler(IRustServerQuery query, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class TimeCommandHandler(IRustServerQuery query, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "time";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/UnmuteCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/UnmuteCommandHandler.cs
index cd9200b..b39b223 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/UnmuteCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/UnmuteCommandHandler.cs
@@ -1,5 +1,5 @@
using RustPlusBot.Features.Commands.Dispatching;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Commands;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -7,7 +7,7 @@ namespace RustPlusBot.Features.Commands.Handlers;
/// !unmute — re-enable bot-to-game output.
/// The mute store.
/// The reply localizer.
-internal sealed class UnmuteCommandHandler(IMuteStore store, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class UnmuteCommandHandler(IMuteStore store, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "unmute";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/UptimeCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/UptimeCommandHandler.cs
index 2d338fb..68553ae 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/UptimeCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/UptimeCommandHandler.cs
@@ -1,14 +1,14 @@
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
using RustPlusBot.Features.Commands.Hosting;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
/// !uptime — reports how long the bot process has been running.
/// The process-uptime baseline.
/// The reply localizer.
-internal sealed class UptimeCommandHandler(BotUptime uptime, ICommandLocalizer localizer) : ICommandHandler
+internal sealed class UptimeCommandHandler(BotUptime uptime, ILocalizer localizer) : ICommandHandler
{
///
public string Name => "uptime";
diff --git a/src/RustPlusBot.Features.Commands/Handlers/WipeCommandHandler.cs b/src/RustPlusBot.Features.Commands/Handlers/WipeCommandHandler.cs
index d956ed8..e6ce1f6 100644
--- a/src/RustPlusBot.Features.Commands/Handlers/WipeCommandHandler.cs
+++ b/src/RustPlusBot.Features.Commands/Handlers/WipeCommandHandler.cs
@@ -2,7 +2,7 @@
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Formatting;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Handlers;
@@ -10,7 +10,7 @@ namespace RustPlusBot.Features.Commands.Handlers;
/// The live server query.
/// The reply localizer.
/// The clock used to compute the elapsed time since wipe.
-internal sealed class WipeCommandHandler(IRustServerQuery query, ICommandLocalizer localizer, IClock clock)
+internal sealed class WipeCommandHandler(IRustServerQuery query, ILocalizer localizer, IClock clock)
: ICommandHandler
{
///
diff --git a/src/RustPlusBot.Features.Commands/Help/HelpEmbedRenderer.cs b/src/RustPlusBot.Features.Commands/Help/HelpEmbedRenderer.cs
index 587a2de..a50f343 100644
--- a/src/RustPlusBot.Features.Commands/Help/HelpEmbedRenderer.cs
+++ b/src/RustPlusBot.Features.Commands/Help/HelpEmbedRenderer.cs
@@ -1,12 +1,12 @@
using System.Globalization;
using Discord;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Help;
/// Builds the /help embed from the catalog, the server prefix, and the guild culture.
/// Resolves the title, group headings, and per-command descriptions.
-internal sealed class HelpEmbedRenderer(ICommandLocalizer localizer)
+internal sealed class HelpEmbedRenderer(ILocalizer localizer)
{
private static readonly (CommandGroup Group, string HeadingKey)[] GroupOrder =
[
diff --git a/src/RustPlusBot.Features.Commands/Leader/LeaderService.cs b/src/RustPlusBot.Features.Commands/Leader/LeaderService.cs
index 41063d7..b8008bf 100644
--- a/src/RustPlusBot.Features.Commands/Leader/LeaderService.cs
+++ b/src/RustPlusBot.Features.Commands/Leader/LeaderService.cs
@@ -1,5 +1,5 @@
using RustPlusBot.Abstractions.Connections;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Leader;
@@ -17,7 +17,7 @@ internal sealed record LeaderTeamResult(IReadOnlyList Member
/// The testable logic behind /leader: fetch live members, then promote one.
/// The live-server query seam.
/// Resolves the result/error messages.
-internal sealed class LeaderService(IRustServerQuery query, ICommandLocalizer localizer)
+internal sealed class LeaderService(IRustServerQuery query, ILocalizer localizer)
{
/// Fetches the current team for the promote select, or an error message.
/// The owning guild snowflake.
diff --git a/src/RustPlusBot.Features.Commands/Localization/CommandLocalizationCatalog.cs b/src/RustPlusBot.Features.Commands/Localization/CommandLocalizationCatalog.cs
deleted file mode 100644
index cf74ea3..0000000
--- a/src/RustPlusBot.Features.Commands/Localization/CommandLocalizationCatalog.cs
+++ /dev/null
@@ -1,185 +0,0 @@
-namespace RustPlusBot.Features.Commands.Localization;
-
-/// The in-memory string catalog for command replies: culture -> (key -> value). English is the fallback.
-internal sealed class CommandLocalizationCatalog
-{
- /// culture -> key -> value.
- public required IReadOnlyDictionary> Strings { get; init; }
-
- /// The built-in EN/FR catalog.
- public static CommandLocalizationCatalog Default { get; } = new()
- {
- Strings = new Dictionary>(StringComparer.Ordinal)
- {
- ["en"] = new Dictionary(StringComparer.Ordinal)
- {
- ["command.mute.done"] = "Bot muted.",
- ["command.unmute.done"] = "Bot unmuted.",
- ["command.uptime.ok"] = "Uptime: {0}",
- ["command.pop.ok"] = "Pop: {0}/{1} ({2} queued)",
- ["command.time.ok"] = "Time: {0} — {1}",
- ["command.time.day"] = "day",
- ["command.time.night"] = "night",
- ["command.wipe.ok"] = "Wiped {0} ago",
- ["command.notconnected"] = "Not connected to the server.",
- ["command.wipe.unknown"] = "Wipe time is unknown.",
- ["command.online.ok"] = "Online ({0}): {1}",
- ["command.online.none"] = "No one is online.",
- ["command.offline.ok"] = "Offline ({0}): {1}",
- ["command.offline.none"] = "Everyone is online.",
- ["command.team.ok"] = "Team ({0}): {1}",
- ["command.team.none"] = "No team members.",
- ["command.team.nomatch"] = "No teammate matches '{0}'.",
- ["command.steamid.ok"] = "{0}",
- ["command.afk.ok"] = "AFK: {0}",
- ["command.afk.none"] = "Nobody is AFK.",
- ["command.afk.member"] = "{0} ({1})",
- ["command.alive.ok"] = "Alive: {0}",
- ["command.alive.dead"] = "{0} dead",
- ["command.alive.member"] = "{0} {1}",
- ["command.prox.ok"] = "Prox: {0}",
- ["command.prox.member"] = "{0} {1}m",
- ["command.prox.selfunknown"] = "Can't locate you.",
- ["command.prox.alone"] = "No teammates nearby.",
- ["command.cargo.ok"] = "Cargo Ship at {0} ({1} ago)",
- ["command.cargo.none"] = "No cargo ship on the map.",
- ["command.heli.ok"] = "Patrol Helicopter at {0} ({1} ago)",
- ["command.heli.none"] = "No patrol helicopter on the map.",
- ["command.chinook.ok"] = "Chinook at {0} ({1} ago)",
- ["command.chinook.none"] = "No chinook on the map.",
- ["command.events.ok"] = "Recent: {0}",
- ["command.events.none"] = "No recent events.",
- ["command.event.cargoentered"] = "cargo in {0}",
- ["command.event.cargoleft"] = "cargo left {0}",
- ["command.event.helientered"] = "heli in {0}",
- ["command.event.helileft"] = "heli left {0}",
- ["command.event.chinookspawned"] = "chinook in {0}",
- ["command.small.online"] = "Small Oil Rig: crate ready, waiting for activation.",
- ["command.small.active"] = "Small Oil Rig: combat phase — crate lootable in {0}.",
- ["command.small.offline"] = "Small Oil Rig: looted / dormant — respawns in {0}.",
- ["command.large.online"] = "Large Oil Rig: crate ready, waiting for activation.",
- ["command.large.active"] = "Large Oil Rig: combat phase — crate lootable in {0}.",
- ["command.large.offline"] = "Large Oil Rig: looted / dormant — respawns in {0}.",
- ["command.server.none"] = "No server is set up yet.",
- ["command.server.specify"] = "Multiple servers are set up — choose one with the server option.",
- ["command.server.unknown"] = "That server isn't set up.",
- ["help.title"] = "Commands",
- ["help.group.control"] = "Control",
- ["help.group.server"] = "Server",
- ["help.group.teamintel"] = "Team",
- ["help.group.bot"] = "Bot",
- ["help.group.slash"] = "Slash commands",
- ["help.prefixnote"] = "Prefixes are configured per server; showing the default.",
- ["help.mute"] = "Silence all bot output to the game.",
- ["help.unmute"] = "Resume bot output to the game.",
- ["help.pop"] = "Show the server population.",
- ["help.time"] = "Show the in-game time.",
- ["help.wipe"] = "Show how long ago the server wiped.",
- ["help.online"] = "List online teammates.",
- ["help.offline"] = "List offline teammates.",
- ["help.team"] = "List all team members.",
- ["help.steamid"] = "Show teammates' Steam IDs.",
- ["help.alive"] = "Show how long each teammate has survived.",
- ["help.afk"] = "List teammates who are AFK.",
- ["help.prox"] = "Show distance to each teammate.",
- ["help.uptime"] = "Show how long the bot has been running.",
- ["help.slash.help"] = "Show this help message.",
- ["help.slash.uptime"] = "Show the bot's uptime.",
- ["help.slash.leader"] = "Transfer in-game team leadership.",
- ["uptime.ok"] = "Bot uptime: {0}",
- ["leader.noserver"] = "No server is set up yet.",
- ["leader.notconnected"] = "Not connected to this server.",
- ["leader.noteam"] = "No team members to promote.",
- ["leader.pickserver"] = "Choose a server:",
- ["leader.pickmember"] = "Choose who to promote to team leader:",
- ["leader.success"] = "Promoted {0} to team leader.",
- ["leader.failure"] = "Couldn't promote — the server may be unreachable.",
- },
- ["fr"] = new Dictionary(StringComparer.Ordinal)
- {
- ["command.mute.done"] = "Bot mis en sourdine.",
- ["command.unmute.done"] = "Bot réactivé.",
- ["command.uptime.ok"] = "Disponibilité : {0}",
- ["command.pop.ok"] = "Population : {0}/{1} ({2} en file)",
- ["command.time.ok"] = "Heure : {0} — {1}",
- ["command.time.day"] = "jour",
- ["command.time.night"] = "nuit",
- ["command.wipe.ok"] = "Wipe il y a {0}",
- ["command.notconnected"] = "Non connecté au serveur.",
- ["command.wipe.unknown"] = "Heure de wipe inconnue.",
- ["command.online.ok"] = "En ligne ({0}) : {1}",
- ["command.online.none"] = "Personne n'est en ligne.",
- ["command.offline.ok"] = "Hors ligne ({0}) : {1}",
- ["command.offline.none"] = "Tout le monde est en ligne.",
- ["command.team.ok"] = "Équipe ({0}) : {1}",
- ["command.team.none"] = "Aucun membre d'équipe.",
- ["command.team.nomatch"] = "Aucun coéquipier ne correspond à « {0} ».",
- ["command.steamid.ok"] = "{0}",
- ["command.afk.ok"] = "AFK : {0}",
- ["command.afk.none"] = "Personne n'est AFK.",
- ["command.afk.member"] = "{0} ({1})",
- ["command.alive.ok"] = "En vie : {0}",
- ["command.alive.dead"] = "{0} mort",
- ["command.alive.member"] = "{0} {1}",
- ["command.prox.ok"] = "Prox : {0}",
- ["command.prox.member"] = "{0} {1}m",
- ["command.prox.selfunknown"] = "Impossible de vous localiser.",
- ["command.prox.alone"] = "Aucun coéquipier à proximité.",
- ["command.cargo.ok"] = "Cargo en {0} (il y a {1})",
- ["command.cargo.none"] = "Aucun cargo sur la carte.",
- ["command.heli.ok"] = "Hélicoptère en {0} (il y a {1})",
- ["command.heli.none"] = "Aucun hélicoptère sur la carte.",
- ["command.chinook.ok"] = "Chinook en {0} (il y a {1})",
- ["command.chinook.none"] = "Aucun chinook sur la carte.",
- ["command.events.ok"] = "Récent : {0}",
- ["command.events.none"] = "Aucun événement récent.",
- ["command.event.cargoentered"] = "cargo en {0}",
- ["command.event.cargoleft"] = "cargo parti de {0}",
- ["command.event.helientered"] = "heli en {0}",
- ["command.event.helileft"] = "heli parti de {0}",
- ["command.event.chinookspawned"] = "chinook en {0}",
- ["command.small.online"] = "Petite plateforme : caisse prête, en attente d'activation.",
- ["command.small.active"] = "Petite plateforme : phase de combat — caisse lootable dans {0}.",
- ["command.small.offline"] = "Petite plateforme : pillée / dormante — réapparaît dans {0}.",
- ["command.large.online"] = "Grande plateforme : caisse prête, en attente d'activation.",
- ["command.large.active"] = "Grande plateforme : phase de combat — caisse lootable dans {0}.",
- ["command.large.offline"] = "Grande plateforme : pillée / dormante — réapparaît dans {0}.",
- ["command.server.none"] = "Aucun serveur n'est encore configuré.",
- ["command.server.specify"] =
- "Plusieurs serveurs sont configurés — choisissez-en un avec l'option serveur.",
- ["command.server.unknown"] = "Ce serveur n'est pas configuré.",
- ["help.title"] = "Commandes",
- ["help.group.control"] = "Contrôle",
- ["help.group.server"] = "Serveur",
- ["help.group.teamintel"] = "Équipe",
- ["help.group.bot"] = "Bot",
- ["help.group.slash"] = "Commandes slash",
- ["help.prefixnote"] = "Les préfixes sont configurés par serveur ; affichage du préfixe par défaut.",
- ["help.mute"] = "Couper toute sortie du bot vers le jeu.",
- ["help.unmute"] = "Réactiver la sortie du bot vers le jeu.",
- ["help.pop"] = "Afficher la population du serveur.",
- ["help.time"] = "Afficher l'heure en jeu.",
- ["help.wipe"] = "Afficher depuis combien de temps le serveur a été wipe.",
- ["help.online"] = "Lister les coéquipiers en ligne.",
- ["help.offline"] = "Lister les coéquipiers hors ligne.",
- ["help.team"] = "Lister tous les membres de l'équipe.",
- ["help.steamid"] = "Afficher les identifiants Steam des coéquipiers.",
- ["help.alive"] = "Afficher depuis combien de temps chaque coéquipier survit.",
- ["help.afk"] = "Lister les coéquipiers qui sont AFK.",
- ["help.prox"] = "Afficher la distance à chaque coéquipier.",
- ["help.uptime"] = "Afficher depuis combien de temps le bot fonctionne.",
- ["help.slash.help"] = "Afficher ce message d'aide.",
- ["help.slash.uptime"] = "Afficher la disponibilité du bot.",
- ["help.slash.leader"] = "Transférer le rôle de chef d'équipe en jeu.",
- ["uptime.ok"] = "Disponibilité du bot : {0}",
- ["leader.noserver"] = "Aucun serveur n'est encore configuré.",
- ["leader.notconnected"] = "Non connecté à ce serveur.",
- ["leader.noteam"] = "Aucun membre d'équipe à promouvoir.",
- ["leader.pickserver"] = "Choisissez un serveur :",
- ["leader.pickmember"] = "Choisissez qui promouvoir chef d'équipe :",
- ["leader.success"] = "{0} promu chef d'équipe.",
- ["leader.failure"] = "Impossible de promouvoir — le serveur est peut-être injoignable.",
- },
- },
- };
-}
diff --git a/src/RustPlusBot.Features.Commands/Localization/CommandLocalizer.cs b/src/RustPlusBot.Features.Commands/Localization/CommandLocalizer.cs
deleted file mode 100644
index 8acc4cf..0000000
--- a/src/RustPlusBot.Features.Commands/Localization/CommandLocalizer.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Commands.Localization;
-
-/// In-game reply localizer backed by the shared .
-/// The string catalog.
-internal sealed class CommandLocalizer(CommandLocalizationCatalog catalog)
- : DictionaryLocalizer(catalog.Strings), ICommandLocalizer;
diff --git a/src/RustPlusBot.Features.Commands/Localization/ICommandLocalizer.cs b/src/RustPlusBot.Features.Commands/Localization/ICommandLocalizer.cs
deleted file mode 100644
index 0bc28d1..0000000
--- a/src/RustPlusBot.Features.Commands/Localization/ICommandLocalizer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Commands.Localization;
-
-/// Resolves localized in-game reply strings by key and culture, falling back to English.
-internal interface ICommandLocalizer : ILocalizer;
diff --git a/src/RustPlusBot.Features.Commands/Modules/CommandSurfaceModule.cs b/src/RustPlusBot.Features.Commands/Modules/CommandSurfaceModule.cs
index b314e64..1e99ac8 100644
--- a/src/RustPlusBot.Features.Commands/Modules/CommandSurfaceModule.cs
+++ b/src/RustPlusBot.Features.Commands/Modules/CommandSurfaceModule.cs
@@ -6,7 +6,7 @@
using RustPlusBot.Features.Commands.Help;
using RustPlusBot.Features.Commands.Hosting;
using RustPlusBot.Features.Commands.Leader;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Commands;
using RustPlusBot.Persistence.Servers;
using RustPlusBot.Persistence.Workspace;
@@ -76,7 +76,7 @@ public async Task UptimeAsync()
await using (scope.ConfigureAwait(false))
{
var workspace = scope.ServiceProvider.GetRequiredService();
- var localizer = scope.ServiceProvider.GetRequiredService();
+ var localizer = scope.ServiceProvider.GetRequiredService();
var uptime = scope.ServiceProvider.GetRequiredService();
var culture = await workspace.GetCultureAsync(Context.Guild.Id).ConfigureAwait(false);
@@ -103,7 +103,7 @@ public async Task LeaderAsync()
var workspace = scope.ServiceProvider.GetRequiredService();
var servers = scope.ServiceProvider.GetRequiredService();
var leader = scope.ServiceProvider.GetRequiredService();
- var localizer = scope.ServiceProvider.GetRequiredService();
+ var localizer = scope.ServiceProvider.GetRequiredService();
var culture = await workspace.GetCultureAsync(Context.Guild.Id).ConfigureAwait(false);
var known = await servers.ListAsync(Context.Guild.Id).ConfigureAwait(false);
@@ -137,7 +137,7 @@ await ShowMemberSelectAsync(scope.ServiceProvider, leader, localizer, Context.Gu
private async Task ShowMemberSelectAsync(
IServiceProvider provider,
LeaderService leader,
- ICommandLocalizer localizer,
+ ILocalizer localizer,
ulong guildId,
Guid serverId,
string culture)
diff --git a/src/RustPlusBot.Features.Commands/Modules/LeaderComponentModule.cs b/src/RustPlusBot.Features.Commands/Modules/LeaderComponentModule.cs
index 5184719..df6c268 100644
--- a/src/RustPlusBot.Features.Commands/Modules/LeaderComponentModule.cs
+++ b/src/RustPlusBot.Features.Commands/Modules/LeaderComponentModule.cs
@@ -4,7 +4,7 @@
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using RustPlusBot.Features.Commands.Leader;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Servers;
using RustPlusBot.Persistence.Workspace;
@@ -39,7 +39,7 @@ public async Task OnServerSelectedAsync(string[] values)
{
var workspace = scope.ServiceProvider.GetRequiredService();
var leader = scope.ServiceProvider.GetRequiredService();
- var localizer = scope.ServiceProvider.GetRequiredService();
+ var localizer = scope.ServiceProvider.GetRequiredService();
var culture = await workspace.GetCultureAsync(Context.Guild.Id).ConfigureAwait(false);
var result = await leader.GetMembersAsync(Context.Guild.Id, serverId, culture, CancellationToken.None)
diff --git a/src/RustPlusBot.Features.Commands/Servers/ServerResolver.cs b/src/RustPlusBot.Features.Commands/Servers/ServerResolver.cs
index f49855b..4674379 100644
--- a/src/RustPlusBot.Features.Commands/Servers/ServerResolver.cs
+++ b/src/RustPlusBot.Features.Commands/Servers/ServerResolver.cs
@@ -1,4 +1,4 @@
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Servers;
namespace RustPlusBot.Features.Commands.Servers;
@@ -6,7 +6,7 @@ namespace RustPlusBot.Features.Commands.Servers;
/// Picks the target server for a slash command from an optional (autocompleted) server argument.
/// The server service used to list the guild's servers.
/// Resolves the error messages.
-internal sealed class ServerResolver(IServerService servers, ICommandLocalizer localizer)
+internal sealed class ServerResolver(IServerService servers, ILocalizer localizer)
{
/// Resolves the target server, or returns a localized error.
/// The owning guild snowflake.
diff --git a/src/RustPlusBot.Features.Events/EventServiceCollectionExtensions.cs b/src/RustPlusBot.Features.Events/EventServiceCollectionExtensions.cs
index 8f26890..95cc824 100644
--- a/src/RustPlusBot.Features.Events/EventServiceCollectionExtensions.cs
+++ b/src/RustPlusBot.Features.Events/EventServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@
using RustPlusBot.Features.Events.Relaying;
using RustPlusBot.Features.Events.Rendering;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Events;
@@ -22,8 +23,7 @@ public static IServiceCollection AddEvents(this IServiceCollection services)
services.AddSingleton(sp => sp.GetRequiredService());
services.AddSingleton();
services.AddSingleton(sp => sp.GetRequiredService());
- services.AddSingleton(EventLocalizationCatalog.Default);
- services.AddSingleton();
+ services.AddRustPlusBotLocalization();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/RustPlusBot.Features.Events/Rendering/EventEmbedRenderer.cs b/src/RustPlusBot.Features.Events/Rendering/EventEmbedRenderer.cs
index d100d15..10c52a2 100644
--- a/src/RustPlusBot.Features.Events/Rendering/EventEmbedRenderer.cs
+++ b/src/RustPlusBot.Features.Events/Rendering/EventEmbedRenderer.cs
@@ -2,12 +2,13 @@
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Events.Classifying;
using RustPlusBot.Features.Events.Formatting;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Events.Rendering;
/// Renders one as a Discord embed.
/// The reply localizer.
-internal sealed class EventEmbedRenderer(IEventLocalizer localizer)
+internal sealed class EventEmbedRenderer(ILocalizer localizer)
{
/// Renders the event for a guild culture.
/// The event to render.
diff --git a/src/RustPlusBot.Features.Events/Rendering/EventLocalizationCatalog.cs b/src/RustPlusBot.Features.Events/Rendering/EventLocalizationCatalog.cs
deleted file mode 100644
index c58ed03..0000000
--- a/src/RustPlusBot.Features.Events/Rendering/EventLocalizationCatalog.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-namespace RustPlusBot.Features.Events.Rendering;
-
-/// The in-memory string catalog for live events: culture -> (key -> value). English is the fallback.
-internal sealed class EventLocalizationCatalog
-{
- /// culture -> key -> value.
- public required IReadOnlyDictionary> Strings { get; init; }
-
- /// The built-in EN/FR catalog.
- public static EventLocalizationCatalog Default { get; } = new()
- {
- Strings = new Dictionary>(StringComparer.Ordinal)
- {
- ["en"] = new Dictionary(StringComparer.Ordinal)
- {
- ["event.cargo.entered"] = "🚢 Cargo Ship entered at {0}",
- ["event.cargo.left"] = "🚢 Cargo Ship left ({0})",
- ["event.heli.entered"] = "🚁 Patrol Helicopter entered at {0}",
- ["event.heli.left"] = "🚁 Patrol Helicopter left ({0})",
- ["event.chinook.spawned"] = "🚁 Chinook spawned at {0}",
- ["event.title"] = "Live event",
- ["event.cargo.entered.line"] = "Cargo Ship entered at {0}",
- ["event.cargo.left.line"] = "Cargo Ship left ({0})",
- ["event.heli.entered.line"] = "Patrol Helicopter entered at {0}",
- ["event.heli.left.line"] = "Patrol Helicopter left ({0})",
- ["event.chinook.spawned.line"] = "Chinook spawned at {0}",
- ["event.rig.small.activated"] = "🛢️ Small Oil Rig activated — combat phase, crate lootable soon ({0})",
- ["event.rig.small.lootable"] = "🛢️ Small Oil Rig — crate is now LOOTABLE ({0})",
- ["event.rig.small.respawned"] = "🛢️ Small Oil Rig — crate respawned, armed again ({0})",
- ["event.rig.large.activated"] = "🛢️ Large Oil Rig activated — combat phase, crate lootable soon ({0})",
- ["event.rig.large.lootable"] = "🛢️ Large Oil Rig — crate is now LOOTABLE ({0})",
- ["event.rig.large.respawned"] = "🛢️ Large Oil Rig — crate respawned, armed again ({0})",
- ["event.rig.small.activated.line"] = "Small Oil Rig activated — combat phase ({0})",
- ["event.rig.small.lootable.line"] = "Small Oil Rig — crate is now lootable ({0})",
- ["event.rig.small.respawned.line"] = "Small Oil Rig — crate respawned ({0})",
- ["event.rig.large.activated.line"] = "Large Oil Rig activated — combat phase ({0})",
- ["event.rig.large.lootable.line"] = "Large Oil Rig — crate is now lootable ({0})",
- ["event.rig.large.respawned.line"] = "Large Oil Rig — crate respawned ({0})",
- },
- ["fr"] = new Dictionary(StringComparer.Ordinal)
- {
- ["event.cargo.entered"] = "🚢 Cargo Ship arrivé en {0}",
- ["event.cargo.left"] = "🚢 Cargo Ship parti ({0})",
- ["event.heli.entered"] = "🚁 Hélicoptère de patrouille arrivé en {0}",
- ["event.heli.left"] = "🚁 Hélicoptère de patrouille parti ({0})",
- ["event.chinook.spawned"] = "🚁 Chinook apparu en {0}",
- ["event.title"] = "Événement",
- ["event.cargo.entered.line"] = "Cargo Ship arrivé en {0}",
- ["event.cargo.left.line"] = "Cargo Ship parti ({0})",
- ["event.heli.entered.line"] = "Hélicoptère de patrouille arrivé en {0}",
- ["event.heli.left.line"] = "Hélicoptère de patrouille parti ({0})",
- ["event.chinook.spawned.line"] = "Chinook apparu en {0}",
- ["event.rig.small.activated"] =
- "🛢️ Petite plateforme pétrolière activée — phase de combat, caisse bientôt lootable ({0})",
- ["event.rig.small.lootable"] = "🛢️ Petite plateforme pétrolière — caisse LOOTABLE ({0})",
- ["event.rig.small.respawned"] = "🛢️ Petite plateforme pétrolière — caisse réapparue, réarmée ({0})",
- ["event.rig.large.activated"] =
- "🛢️ Grande plateforme pétrolière activée — phase de combat, caisse bientôt lootable ({0})",
- ["event.rig.large.lootable"] = "🛢️ Grande plateforme pétrolière — caisse LOOTABLE ({0})",
- ["event.rig.large.respawned"] = "🛢️ Grande plateforme pétrolière — caisse réapparue, réarmée ({0})",
- ["event.rig.small.activated.line"] = "Petite plateforme activée — phase de combat ({0})",
- ["event.rig.small.lootable.line"] = "Petite plateforme — caisse lootable ({0})",
- ["event.rig.small.respawned.line"] = "Petite plateforme — caisse réapparue ({0})",
- ["event.rig.large.activated.line"] = "Grande plateforme activée — phase de combat ({0})",
- ["event.rig.large.lootable.line"] = "Grande plateforme — caisse lootable ({0})",
- ["event.rig.large.respawned.line"] = "Grande plateforme — caisse réapparue ({0})",
- },
- },
- };
-}
diff --git a/src/RustPlusBot.Features.Events/Rendering/EventLocalizer.cs b/src/RustPlusBot.Features.Events/Rendering/EventLocalizer.cs
deleted file mode 100644
index 9335d43..0000000
--- a/src/RustPlusBot.Features.Events/Rendering/EventLocalizer.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Events.Rendering;
-
-/// Live-event localizer backed by the shared .
-/// The string catalog.
-internal sealed class EventLocalizer(EventLocalizationCatalog catalog)
- : DictionaryLocalizer(catalog.Strings), IEventLocalizer;
diff --git a/src/RustPlusBot.Features.Events/Rendering/IEventLocalizer.cs b/src/RustPlusBot.Features.Events/Rendering/IEventLocalizer.cs
deleted file mode 100644
index c87ddb5..0000000
--- a/src/RustPlusBot.Features.Events/Rendering/IEventLocalizer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Events.Rendering;
-
-/// Resolves localized live-event strings by key and culture, falling back to English.
-internal interface IEventLocalizer : ILocalizer;
diff --git a/src/RustPlusBot.Features.Map/Assets/MapIcons.cs b/src/RustPlusBot.Features.Map/Assets/MapIcons.cs
index 0f51e0e..fc58d91 100644
--- a/src/RustPlusBot.Features.Map/Assets/MapIcons.cs
+++ b/src/RustPlusBot.Features.Map/Assets/MapIcons.cs
@@ -40,14 +40,14 @@ public static class MapIcons
/// The parameter is reserved for future use: the renderer is responsible
/// for overlaying activation styling; this method always returns the base icon regardless of state.
///
-#pragma warning disable RCS1163 // Unused parameter — 'active' is part of the public API contract; activation styling is applied by the renderer, not here.
+#pragma warning disable RCS1163, IDE0060 // Unused parameter — 'active' is part of the public API contract; activation styling is applied by the renderer, not here.
public static Image? Rig(RigKind kind, bool active) => kind switch
{
RigKind.Small => Load("oilrig"),
RigKind.Large => Load("largeoilrig"),
_ => null,
};
-#pragma warning restore RCS1163
+#pragma warning restore RCS1163, IDE0060
/// Gets the travelling vendor icon.
/// The cached vendor icon image, or null.
diff --git a/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs b/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs
index d13cab4..5b39327 100644
--- a/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs
+++ b/src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs
@@ -260,11 +260,13 @@ private sealed class Handle : IAsyncDisposable
private readonly CancellationTokenSource _cts;
private readonly Task _runTask;
+#pragma warning disable IDE0290 // A primary constructor trips CA2000 on the CancellationTokenSource field; the explicit ctor is intentional.
public Handle(CancellationTokenSource cts, Task runTask)
{
_cts = cts;
_runTask = runTask;
}
+#pragma warning restore IDE0290
public ValueTask DisposeAsync()
{
diff --git a/src/RustPlusBot.Features.Players/PlayerEventServiceCollectionExtensions.cs b/src/RustPlusBot.Features.Players/PlayerEventServiceCollectionExtensions.cs
index 304e840..5642eed 100644
--- a/src/RustPlusBot.Features.Players/PlayerEventServiceCollectionExtensions.cs
+++ b/src/RustPlusBot.Features.Players/PlayerEventServiceCollectionExtensions.cs
@@ -3,6 +3,7 @@
using RustPlusBot.Features.Players.Posting;
using RustPlusBot.Features.Players.Relaying;
using RustPlusBot.Features.Players.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Players;
@@ -16,8 +17,7 @@ public static IServiceCollection AddPlayers(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
- services.AddSingleton(PlayerLocalizationCatalog.Default);
- services.AddSingleton();
+ services.AddRustPlusBotLocalization();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/RustPlusBot.Features.Players/Rendering/IPlayerLocalizer.cs b/src/RustPlusBot.Features.Players/Rendering/IPlayerLocalizer.cs
deleted file mode 100644
index 818dfdc..0000000
--- a/src/RustPlusBot.Features.Players/Rendering/IPlayerLocalizer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Players.Rendering;
-
-/// Resolves localized player-event strings by key and culture, falling back to English.
-internal interface IPlayerLocalizer : ILocalizer;
diff --git a/src/RustPlusBot.Features.Players/Rendering/PlayerEventRenderer.cs b/src/RustPlusBot.Features.Players/Rendering/PlayerEventRenderer.cs
index c2babbc..45130cc 100644
--- a/src/RustPlusBot.Features.Players/Rendering/PlayerEventRenderer.cs
+++ b/src/RustPlusBot.Features.Players/Rendering/PlayerEventRenderer.cs
@@ -2,12 +2,13 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Events.Formatting;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Players.Rendering;
/// Renders one as a Discord embed or an in-game line.
/// The reply localizer.
-internal sealed class PlayerEventRenderer(IPlayerLocalizer localizer)
+internal sealed class PlayerEventRenderer(ILocalizer localizer)
{
/// Renders the transition for a guild culture as a Discord embed.
/// The player transition to render.
diff --git a/src/RustPlusBot.Features.Players/Rendering/PlayerLocalizationCatalog.cs b/src/RustPlusBot.Features.Players/Rendering/PlayerLocalizationCatalog.cs
deleted file mode 100644
index 9b946dd..0000000
--- a/src/RustPlusBot.Features.Players/Rendering/PlayerLocalizationCatalog.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-namespace RustPlusBot.Features.Players.Rendering;
-
-/// The in-memory string catalog for player events: culture -> (key -> value). English is the fallback.
-internal sealed class PlayerLocalizationCatalog
-{
- /// culture -> key -> value.
- public required IReadOnlyDictionary> Strings { get; init; }
-
- /// The built-in EN/FR catalog.
- public static PlayerLocalizationCatalog Default { get; } = new()
- {
- Strings = new Dictionary>(StringComparer.Ordinal)
- {
- ["en"] = new Dictionary(StringComparer.Ordinal)
- {
- ["player.title"] = "Team event",
- ["player.connect"] = "🟢 {0} connected",
- ["player.connect.line"] = "{0} connected",
- ["player.disconnect"] = "🔴 {0} disconnected",
- ["player.disconnect.line"] = "{0} disconnected",
- ["player.death"] = "💀 {0} died at {1}",
- ["player.death.line"] = "{0} died at {1}",
- ["player.death.unknown"] = "💀 {0} died",
- ["player.death.unknown.line"] = "{0} died",
- ["player.respawn"] = "✨ {0} respawned at {1}",
- ["player.respawn.line"] = "{0} respawned at {1}",
- ["player.afk"] = "💤 {0} is AFK ({1})",
- ["player.afk.line"] = "{0} is AFK ({1})",
- ["player.afk.back"] = "👋 {0} is back",
- ["player.afk.back.line"] = "{0} is back",
- },
- ["fr"] = new Dictionary(StringComparer.Ordinal)
- {
- ["player.title"] = "Événement d'équipe",
- ["player.connect"] = "🟢 {0} s'est connecté",
- ["player.connect.line"] = "{0} s'est connecté",
- ["player.disconnect"] = "🔴 {0} s'est déconnecté",
- ["player.disconnect.line"] = "{0} s'est déconnecté",
- ["player.death"] = "💀 {0} est mort en {1}",
- ["player.death.line"] = "{0} est mort en {1}",
- ["player.death.unknown"] = "💀 {0} est mort",
- ["player.death.unknown.line"] = "{0} est mort",
- ["player.respawn"] = "✨ {0} a réapparu en {1}",
- ["player.respawn.line"] = "{0} a réapparu en {1}",
- ["player.afk"] = "💤 {0} est AFK ({1})",
- ["player.afk.line"] = "{0} est AFK ({1})",
- ["player.afk.back"] = "👋 {0} est de retour",
- ["player.afk.back.line"] = "{0} est de retour",
- },
- },
- };
-}
diff --git a/src/RustPlusBot.Features.Players/Rendering/PlayerLocalizer.cs b/src/RustPlusBot.Features.Players/Rendering/PlayerLocalizer.cs
deleted file mode 100644
index a841eac..0000000
--- a/src/RustPlusBot.Features.Players/Rendering/PlayerLocalizer.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Players.Rendering;
-
-/// Player-event localizer backed by the shared .
-/// The string catalog.
-internal sealed class PlayerLocalizer(PlayerLocalizationCatalog catalog)
- : DictionaryLocalizer(catalog.Strings), IPlayerLocalizer;
diff --git a/src/RustPlusBot.Features.Switches/Rendering/ISwitchLocalizer.cs b/src/RustPlusBot.Features.Switches/Rendering/ISwitchLocalizer.cs
deleted file mode 100644
index 39b3454..0000000
--- a/src/RustPlusBot.Features.Switches/Rendering/ISwitchLocalizer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Switches.Rendering;
-
-/// Resolves localized smart-switch strings by key and culture, falling back to English.
-internal interface ISwitchLocalizer : ILocalizer;
diff --git a/src/RustPlusBot.Features.Switches/Rendering/SwitchEmbedRenderer.cs b/src/RustPlusBot.Features.Switches/Rendering/SwitchEmbedRenderer.cs
index efdcc70..c7e7a14 100644
--- a/src/RustPlusBot.Features.Switches/Rendering/SwitchEmbedRenderer.cs
+++ b/src/RustPlusBot.Features.Switches/Rendering/SwitchEmbedRenderer.cs
@@ -1,11 +1,12 @@
using Discord;
using RustPlusBot.Domain.Switches;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Switches.Rendering;
/// Renders a Smart Switch as a Discord embed + control row, and the pairing-prompt embed + row. Pure.
/// The switch localizer.
-internal sealed class SwitchEmbedRenderer(ISwitchLocalizer localizer)
+internal sealed class SwitchEmbedRenderer(ILocalizer localizer)
{
/// Renders the switch embed and its control buttons. null ⇒ unreachable.
/// The switch.
diff --git a/src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizationCatalog.cs b/src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizationCatalog.cs
deleted file mode 100644
index fe050a0..0000000
--- a/src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizationCatalog.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-namespace RustPlusBot.Features.Switches.Rendering;
-
-/// The in-memory string catalog for Smart Switches: culture -> (key -> value). English is the fallback.
-internal sealed class SwitchLocalizationCatalog
-{
- /// culture -> key -> value.
- public required IReadOnlyDictionary> Strings { get; init; }
-
- /// The built-in EN/FR catalog.
- public static SwitchLocalizationCatalog Default { get; } = new()
- {
- Strings = new Dictionary>(StringComparer.Ordinal)
- {
- ["en"] = new Dictionary(StringComparer.Ordinal)
- {
- ["switch.status.on"] = "⚡ ON",
- ["switch.status.off"] = "⭘ OFF",
- ["switch.status.unreachable"] = "⚠️ Unreachable",
- ["switch.button.on"] = "Turn on",
- ["switch.button.off"] = "Turn off",
- ["switch.button.strobe"] = "Strobe",
- ["switch.button.rename"] = "Rename",
- ["switch.embed.footer"] = "Entity {0}",
- ["switch.prompt.title"] = "New switch detected",
- ["switch.prompt.body"] = "Detected a new Smart Switch ({0}). Add it?",
- ["switch.prompt.accept"] = "Accept",
- ["switch.prompt.dismiss"] = "Dismiss",
- ["switch.prompt.dismissed"] = "Dismissed.",
- ["switch.rename.modal.title"] = "Rename switch",
- ["switch.rename.input.label"] = "Switch name",
- ["switch.unreachable.ephemeral"] = "Switch is unreachable right now.",
- },
- ["fr"] = new Dictionary(StringComparer.Ordinal)
- {
- ["switch.status.on"] = "⚡ ALLUMÉ",
- ["switch.status.off"] = "⭘ ÉTEINT",
- ["switch.status.unreachable"] = "⚠️ Injoignable",
- ["switch.button.on"] = "Allumer",
- ["switch.button.off"] = "Éteindre",
- ["switch.button.strobe"] = "Stroboscope",
- ["switch.button.rename"] = "Renommer",
- ["switch.embed.footer"] = "Entité {0}",
- ["switch.prompt.title"] = "Nouvel interrupteur détecté",
- ["switch.prompt.body"] = "Nouvel interrupteur connecté détecté ({0}). L'ajouter ?",
- ["switch.prompt.accept"] = "Accepter",
- ["switch.prompt.dismiss"] = "Ignorer",
- ["switch.prompt.dismissed"] = "Ignoré.",
- ["switch.rename.modal.title"] = "Renommer l'interrupteur",
- ["switch.rename.input.label"] = "Nom de l'interrupteur",
- ["switch.unreachable.ephemeral"] = "L'interrupteur est injoignable pour le moment.",
- },
- },
- };
-}
diff --git a/src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizer.cs b/src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizer.cs
deleted file mode 100644
index f9d545f..0000000
--- a/src/RustPlusBot.Features.Switches/Rendering/SwitchLocalizer.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Features.Switches.Rendering;
-
-/// Smart-switch localizer backed by the shared .
-/// The string catalog.
-internal sealed class SwitchLocalizer(SwitchLocalizationCatalog catalog)
- : DictionaryLocalizer(catalog.Strings), ISwitchLocalizer;
diff --git a/src/RustPlusBot.Features.Switches/SwitchServiceCollectionExtensions.cs b/src/RustPlusBot.Features.Switches/SwitchServiceCollectionExtensions.cs
index fa17e8f..d35a004 100644
--- a/src/RustPlusBot.Features.Switches/SwitchServiceCollectionExtensions.cs
+++ b/src/RustPlusBot.Features.Switches/SwitchServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@
using RustPlusBot.Features.Switches.Posting;
using RustPlusBot.Features.Switches.Relaying;
using RustPlusBot.Features.Switches.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Switches;
@@ -18,8 +19,7 @@ public static IServiceCollection AddSwitches(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
- services.AddSingleton(SwitchLocalizationCatalog.Default);
- services.AddSingleton();
+ services.AddRustPlusBotLocalization();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/RustPlusBot.Features.Workspace/Localization/ILocalizer.cs b/src/RustPlusBot.Features.Workspace/Localization/ILocalizer.cs
deleted file mode 100644
index 17ff656..0000000
--- a/src/RustPlusBot.Features.Workspace/Localization/ILocalizer.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace RustPlusBot.Features.Workspace.Localization;
-
-/// Resolves localized strings by key and BCP-47 culture, falling back to English.
-internal interface ILocalizer : RustPlusBot.Localization.ILocalizer;
diff --git a/src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs b/src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs
deleted file mode 100644
index de595b3..0000000
--- a/src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-namespace RustPlusBot.Features.Workspace.Localization;
-
-/// The in-memory string catalog: culture -> (key -> value). English is the fallback.
-internal sealed class LocalizationCatalog
-{
- private const string ProductName = "RustPlusBot";
-
- /// culture -> key -> value.
- public required IReadOnlyDictionary> Strings { get; init; }
-
- /// The built-in EN/FR catalog.
- public static LocalizationCatalog Default { get; } = new()
- {
- Strings = new Dictionary>(StringComparer.Ordinal)
- {
- ["en"] = new Dictionary(StringComparer.Ordinal)
- {
- ["category.global.name"] = ProductName,
- ["channel.information.name"] = "information",
- ["channel.setup.name"] = "setup",
- ["channel.settings.name"] = "settings",
- ["channel.info.name"] = "info",
- ["channel.teamchat.name"] = "teamchat",
- ["channel.events.name"] = "events",
- ["channel.map.name"] = "map",
- ["channel.switches.name"] = "switches",
- ["channel.alarms.name"] = "alarms",
- ["information.title"] = ProductName,
- ["information.body"] = "Connect your Rust+ account in #setup, then pair a server in-game to begin.",
- ["information.servers"] = "Servers registered: {0}",
- ["setup.title"] = "Connect your Rust+ account",
- ["setup.body"] =
- "Click **Connect account** below and paste your Rust+ FCM credentials. Then pair a server in-game and its channels appear automatically.",
- ["setup.connect.button"] = "Connect account",
- ["setup.disconnect.button"] = "Disconnect account",
- ["server.info.remove.button"] = "Remove server",
- ["settings.title"] = "Settings",
- ["settings.body"] = "Configure the bot for this server.",
- ["settings.language.label"] = "Language",
- ["server.info.endpoint"] = "Endpoint: {0}:{1}",
- ["server.info.status.label"] = "Status",
- ["server.info.status.connecting"] = "Connecting…",
- ["server.info.status.connected"] = "Connected",
- ["server.info.status.unreachable"] = "Server unreachable",
- ["server.info.status.nocredentials"] = "No working credentials",
- ["server.info.player.label"] = "Active player",
- ["server.info.players.label"] = "Players online",
- ["server.info.team.label"] = "Team",
- ["server.info.team.value"] = "{0}/{1} online · leader {2}",
- ["server.info.swap.placeholder"] = "Switch active player",
- ["server.info.none"] = "—",
- ["map.control.header"] = "Map layers — toggle to show/hide:",
- ["map.layer.grid"] = "Grid",
- ["map.layer.markers"] = "Markers",
- ["map.layer.monuments"] = "Monuments",
- ["map.layer.vendor"] = "Vendor",
- ["map.layer.players"] = "Players",
- ["map.layer.rigs"] = "Rigs",
- },
- ["fr"] = new Dictionary(StringComparer.Ordinal)
- {
- ["category.global.name"] = ProductName,
- ["channel.information.name"] = "informations",
- ["channel.setup.name"] = "configuration",
- ["channel.settings.name"] = "parametres",
- ["channel.info.name"] = "info",
- ["channel.teamchat.name"] = "tchat-equipe",
- ["channel.events.name"] = "evenements",
- ["channel.map.name"] = "carte",
- ["channel.switches.name"] = "interrupteurs",
- ["channel.alarms.name"] = "alarmes",
- ["information.title"] = ProductName,
- ["information.body"] =
- "Connectez votre compte Rust+ dans #configuration, puis appairez un serveur en jeu.",
- ["information.servers"] = "Serveurs enregistres : {0}",
- ["setup.title"] = "Connectez votre compte Rust+",
- ["setup.body"] =
- "Cliquez sur **Connecter le compte** ci-dessous et collez vos identifiants FCM Rust+. Appairez ensuite un serveur en jeu et ses salons apparaitront automatiquement.",
- ["setup.connect.button"] = "Connecter le compte",
- ["setup.disconnect.button"] = "Déconnecter le compte",
- ["server.info.remove.button"] = "Supprimer le serveur",
- ["settings.title"] = "Parametres",
- ["settings.body"] = "Configurez le bot pour ce serveur.",
- ["settings.language.label"] = "Langue",
- ["server.info.endpoint"] = "Adresse : {0}:{1}",
- ["server.info.status.label"] = "État",
- ["server.info.status.connecting"] = "Connexion…",
- ["server.info.status.connected"] = "Connecté",
- ["server.info.status.unreachable"] = "Serveur injoignable",
- ["server.info.status.nocredentials"] = "Aucun identifiant valide",
- ["server.info.player.label"] = "Joueur actif",
- ["server.info.players.label"] = "Joueurs en ligne",
- ["server.info.team.label"] = "Équipe",
- ["server.info.team.value"] = "{0}/{1} en ligne · chef {2}",
- ["server.info.swap.placeholder"] = "Changer le joueur actif",
- ["server.info.none"] = "—",
- ["map.control.header"] = "Calques de la carte — activer/désactiver :",
- ["map.layer.grid"] = "Grille",
- ["map.layer.markers"] = "Marqueurs",
- ["map.layer.monuments"] = "Monuments",
- ["map.layer.vendor"] = "Marchand",
- ["map.layer.players"] = "Joueurs",
- ["map.layer.rigs"] = "Plateformes",
- },
- },
- };
-}
diff --git a/src/RustPlusBot.Features.Workspace/Localization/Localizer.cs b/src/RustPlusBot.Features.Workspace/Localization/Localizer.cs
deleted file mode 100644
index 805b4cf..0000000
--- a/src/RustPlusBot.Features.Workspace/Localization/Localizer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace RustPlusBot.Features.Workspace.Localization;
-
-/// Workspace localizer backed by the shared .
-/// The string catalog.
-internal sealed class Localizer(LocalizationCatalog catalog)
- : RustPlusBot.Localization.DictionaryLocalizer(catalog.Strings), ILocalizer;
diff --git a/src/RustPlusBot.Features.Workspace/Locating/CachingChannelLocator.cs b/src/RustPlusBot.Features.Workspace/Locating/CachingChannelLocator.cs
index 3ef41a0..4a28652 100644
--- a/src/RustPlusBot.Features.Workspace/Locating/CachingChannelLocator.cs
+++ b/src/RustPlusBot.Features.Workspace/Locating/CachingChannelLocator.cs
@@ -14,7 +14,7 @@ internal abstract class CachingChannelLocator(IServiceScopeFactory scopeFactory,
private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30);
private readonly SemaphoreSlim _refreshGate = new(1, 1);
private DateTimeOffset _builtAt = DateTimeOffset.MinValue;
- private Dictionary<(ulong GuildId, Guid ServerId), ulong> _byServer = new();
+ private Dictionary<(ulong GuildId, Guid ServerId), ulong> _byServer = [];
/// A read-only view of the cached (guild, server) → channel-id map, available after ensure-fresh.
protected IReadOnlyDictionary<(ulong GuildId, Guid ServerId), ulong> Entries => _byServer;
@@ -73,7 +73,7 @@ protected async Task EnsureFreshAsync(CancellationToken cancellationToken)
var store = scope.ServiceProvider.GetRequiredService();
var rows = await store.GetChannelsByKeyAsync(channelKey, cancellationToken).ConfigureAwait(false);
- var byServer = new Dictionary<(ulong GuildId, Guid ServerId), ulong>();
+ Dictionary<(ulong GuildId, Guid ServerId), ulong> byServer = [];
foreach (var row in rows)
{
if (row.RustServerId is not { } serverId)
diff --git a/src/RustPlusBot.Features.Workspace/Messages/InformationMessageRenderer.cs b/src/RustPlusBot.Features.Workspace/Messages/InformationMessageRenderer.cs
index e44810a..d45fe19 100644
--- a/src/RustPlusBot.Features.Workspace/Messages/InformationMessageRenderer.cs
+++ b/src/RustPlusBot.Features.Workspace/Messages/InformationMessageRenderer.cs
@@ -1,7 +1,7 @@
using Discord;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Servers;
namespace RustPlusBot.Features.Workspace.Messages;
diff --git a/src/RustPlusBot.Features.Workspace/Messages/MapControlMessageRenderer.cs b/src/RustPlusBot.Features.Workspace/Messages/MapControlMessageRenderer.cs
index 27c9070..3c5113e 100644
--- a/src/RustPlusBot.Features.Workspace/Messages/MapControlMessageRenderer.cs
+++ b/src/RustPlusBot.Features.Workspace/Messages/MapControlMessageRenderer.cs
@@ -1,7 +1,7 @@
using Discord;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Map;
namespace RustPlusBot.Features.Workspace.Messages;
diff --git a/src/RustPlusBot.Features.Workspace/Messages/ServerInfoMessageRenderer.cs b/src/RustPlusBot.Features.Workspace/Messages/ServerInfoMessageRenderer.cs
index 0e69122..2d357e6 100644
--- a/src/RustPlusBot.Features.Workspace/Messages/ServerInfoMessageRenderer.cs
+++ b/src/RustPlusBot.Features.Workspace/Messages/ServerInfoMessageRenderer.cs
@@ -4,8 +4,8 @@
using RustPlusBot.Domain.Connections;
using RustPlusBot.Domain.Credentials;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Connections;
using RustPlusBot.Persistence.Servers;
diff --git a/src/RustPlusBot.Features.Workspace/Messages/SettingsMessageRenderer.cs b/src/RustPlusBot.Features.Workspace/Messages/SettingsMessageRenderer.cs
index 9d7cc34..c8a7668 100644
--- a/src/RustPlusBot.Features.Workspace/Messages/SettingsMessageRenderer.cs
+++ b/src/RustPlusBot.Features.Workspace/Messages/SettingsMessageRenderer.cs
@@ -1,7 +1,7 @@
using Discord;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Workspace.Messages;
diff --git a/src/RustPlusBot.Features.Workspace/Messages/SetupMessageRenderer.cs b/src/RustPlusBot.Features.Workspace/Messages/SetupMessageRenderer.cs
index 24bdf22..d6d1653 100644
--- a/src/RustPlusBot.Features.Workspace/Messages/SetupMessageRenderer.cs
+++ b/src/RustPlusBot.Features.Workspace/Messages/SetupMessageRenderer.cs
@@ -1,7 +1,7 @@
using Discord;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Workspace.Messages;
diff --git a/src/RustPlusBot.Features.Workspace/Reconciler/WorkspaceReconciler.cs b/src/RustPlusBot.Features.Workspace/Reconciler/WorkspaceReconciler.cs
index d274939..feab581 100644
--- a/src/RustPlusBot.Features.Workspace/Reconciler/WorkspaceReconciler.cs
+++ b/src/RustPlusBot.Features.Workspace/Reconciler/WorkspaceReconciler.cs
@@ -1,8 +1,8 @@
using Microsoft.Extensions.Logging;
using RustPlusBot.Domain.Workspace;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Servers;
using RustPlusBot.Persistence.Workspace;
diff --git a/src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs b/src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs
index 1b85963..9cbcd6e 100644
--- a/src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs
+++ b/src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs
@@ -1,13 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using RustPlusBot.Discord;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Locating;
using RustPlusBot.Features.Workspace.Messages;
using RustPlusBot.Features.Workspace.Reconciler;
using RustPlusBot.Features.Workspace.Registry;
using RustPlusBot.Features.Workspace.Specs;
using RustPlusBot.Features.Workspace.Teardown;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Workspace;
@@ -29,8 +29,7 @@ public static IServiceCollection AddWorkspace(this IServiceCollection services)
services.AddSingleton();
// Localization.
- services.AddSingleton(LocalizationCatalog.Default);
- services.AddSingleton();
+ services.AddRustPlusBotLocalization();
// Gateway + per-guild lock.
services.AddSingleton();
diff --git a/src/RustPlusBot.Localization/AssemblyInfo.cs b/src/RustPlusBot.Localization/AssemblyInfo.cs
new file mode 100644
index 0000000..b719b17
--- /dev/null
+++ b/src/RustPlusBot.Localization/AssemblyInfo.cs
@@ -0,0 +1,5 @@
+using System.Resources;
+
+// The neutral Strings.resx holds the English strings, so the runtime treats the
+// neutral culture as English and a missing fr key falls back to it automatically.
+[assembly: NeutralResourcesLanguage("en")]
diff --git a/src/RustPlusBot.Localization/DictionaryLocalizer.cs b/src/RustPlusBot.Localization/DictionaryLocalizer.cs
deleted file mode 100644
index f724562..0000000
--- a/src/RustPlusBot.Localization/DictionaryLocalizer.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Globalization;
-
-namespace RustPlusBot.Localization;
-
-/// Dictionary-backed with English fallback and region normalization.
-/// Culture → (key → value) string table.
-public class DictionaryLocalizer(
- IReadOnlyDictionary> strings) : ILocalizer
-{
- private const string FallbackCulture = "en";
-
- ///
- public string Get(string key, string culture)
- {
- var normalized = Normalize(culture);
- if (strings.TryGetValue(normalized, out var map) && map.TryGetValue(key, out var value))
- {
- return value;
- }
-
- if (strings.TryGetValue(FallbackCulture, out var fallback) &&
- fallback.TryGetValue(key, out var fallbackValue))
- {
- return fallbackValue;
- }
-
- return key;
- }
-
- ///
- public string Get(string key, string culture, params object[] args)
- {
- var format = Get(key, culture);
- var provider = ResolveFormatProvider(Normalize(culture));
- return string.Format(provider, format, args);
- }
-
- private static string Normalize(string culture)
- {
- if (string.IsNullOrWhiteSpace(culture))
- {
- return FallbackCulture;
- }
-
- var dash = culture.IndexOf('-', StringComparison.Ordinal);
- var primary = dash >= 0 ? culture[..dash] : culture;
- return primary.ToLowerInvariant();
- }
-
- private static CultureInfo ResolveFormatProvider(string culture)
- {
- try
- {
- return CultureInfo.GetCultureInfo(culture);
- }
- catch (CultureNotFoundException)
- {
- return CultureInfo.InvariantCulture;
- }
- }
-}
diff --git a/src/RustPlusBot.Localization/LocalizationServiceCollectionExtensions.cs b/src/RustPlusBot.Localization/LocalizationServiceCollectionExtensions.cs
new file mode 100644
index 0000000..7a120ef
--- /dev/null
+++ b/src/RustPlusBot.Localization/LocalizationServiceCollectionExtensions.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace RustPlusBot.Localization;
+
+/// DI registration for the shared localizer.
+public static class LocalizationServiceCollectionExtensions
+{
+ /// Registers the shared (idempotent).
+ /// The service collection.
+ /// The same service collection for chaining.
+ public static IServiceCollection AddRustPlusBotLocalization(this IServiceCollection services)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ services.TryAddSingleton();
+ return services;
+ }
+}
diff --git a/src/RustPlusBot.Localization/ResxLocalizer.cs b/src/RustPlusBot.Localization/ResxLocalizer.cs
new file mode 100644
index 0000000..bfa8699
--- /dev/null
+++ b/src/RustPlusBot.Localization/ResxLocalizer.cs
@@ -0,0 +1,59 @@
+using System.Globalization;
+using System.Resources;
+
+namespace RustPlusBot.Localization;
+
+///
+/// backed by the embedded Strings resource set,
+/// resolving per-call culture with English fallback and region normalization.
+///
+public sealed class ResxLocalizer : ILocalizer
+{
+ private const string FallbackCulture = "en";
+
+ private static readonly ResourceManager Resources =
+ new("RustPlusBot.Localization.Strings", typeof(ResxLocalizer).Assembly);
+
+ ///
+ public string Get(string key, string culture)
+ {
+ var info = ResolveCulture(culture);
+
+ // ResourceManager walks the requested culture down to the neutral (en) set,
+ // so a single lookup already covers the English fallback.
+ var value = Resources.GetString(key, info);
+ return value ?? key;
+ }
+
+ ///
+ public string Get(string key, string culture, params object[] args)
+ {
+ var format = Get(key, culture);
+ return string.Format(ResolveCulture(culture), format, args);
+ }
+
+ private static CultureInfo ResolveCulture(string culture)
+ {
+ var normalized = Normalize(culture);
+ try
+ {
+ return CultureInfo.GetCultureInfo(normalized);
+ }
+ catch (CultureNotFoundException)
+ {
+ return CultureInfo.InvariantCulture;
+ }
+ }
+
+ private static string Normalize(string culture)
+ {
+ if (string.IsNullOrWhiteSpace(culture))
+ {
+ return FallbackCulture;
+ }
+
+ var dash = culture.IndexOf('-', StringComparison.Ordinal);
+ var primary = dash >= 0 ? culture[..dash] : culture;
+ return primary.ToLowerInvariant();
+ }
+}
diff --git a/src/RustPlusBot.Localization/RustPlusBot.Localization.csproj b/src/RustPlusBot.Localization/RustPlusBot.Localization.csproj
index c632161..6f08608 100644
--- a/src/RustPlusBot.Localization/RustPlusBot.Localization.csproj
+++ b/src/RustPlusBot.Localization/RustPlusBot.Localization.csproj
@@ -1,3 +1,16 @@
-
+
+
+
+
+
+
+
+
+
+
+
+ Strings.resx
+
+
diff --git a/src/RustPlusBot.Localization/Strings.fr.resx b/src/RustPlusBot.Localization/Strings.fr.resx
new file mode 100644
index 0000000..6ccfa30
--- /dev/null
+++ b/src/RustPlusBot.Localization/Strings.fr.resx
@@ -0,0 +1,594 @@
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Ping @everyone : désactivé
+
+
+ Ping @everyone : activé
+
+
+ Relais tchat équipe : désactivé
+
+
+ Relais tchat équipe : activé
+
+
+ Renommer
+
+
+ Entité {0}
+
+
+ Déclenchée il y a {0}
+
+
+ Jamais déclenchée
+
+
+ Accepter
+
+
+ Nouvelle alarme connectée détectée ({0}). L'ajouter ?
+
+
+ Ignorer
+
+
+ Nouvelle alarme détectée
+
+
+ Nom de l'alarme
+
+
+ Renommer l'alarme
+
+
+ 🚨 Active
+
+
+ 🔔 Armée
+
+
+ ⚠️ Injoignable
+
+
+ 🚨 {0} déclenchée
+
+
+ RustPlusBot
+
+
+ alarmes
+
+
+ evenements
+
+
+ info
+
+
+ informations
+
+
+ carte
+
+
+ parametres
+
+
+ configuration
+
+
+ interrupteurs
+
+
+ tchat-equipe
+
+
+ {0} ({1})
+
+
+ Personne n'est AFK.
+
+
+ AFK : {0}
+
+
+ {0} mort
+
+
+ {0} {1}
+
+
+ En vie : {0}
+
+
+ Aucun cargo sur la carte.
+
+
+ Cargo en {0} (il y a {1})
+
+
+ Aucun chinook sur la carte.
+
+
+ Chinook en {0} (il y a {1})
+
+
+ cargo en {0}
+
+
+ cargo parti de {0}
+
+
+ chinook en {0}
+
+
+ heli en {0}
+
+
+ heli parti de {0}
+
+
+ Aucun événement récent.
+
+
+ Récent : {0}
+
+
+ Aucun hélicoptère sur la carte.
+
+
+ Hélicoptère en {0} (il y a {1})
+
+
+ Grande plateforme : phase de combat — caisse lootable dans {0}.
+
+
+ Grande plateforme : pillée / dormante — réapparaît dans {0}.
+
+
+ Grande plateforme : caisse prête, en attente d'activation.
+
+
+ Bot mis en sourdine.
+
+
+ Non connecté au serveur.
+
+
+ Tout le monde est en ligne.
+
+
+ Hors ligne ({0}) : {1}
+
+
+ Personne n'est en ligne.
+
+
+ En ligne ({0}) : {1}
+
+
+ Population : {0}/{1} ({2} en file)
+
+
+ Aucun coéquipier à proximité.
+
+
+ {0} {1}m
+
+
+ Prox : {0}
+
+
+ Impossible de vous localiser.
+
+
+ Aucun serveur n'est encore configuré.
+
+
+ Plusieurs serveurs sont configurés — choisissez-en un avec l'option serveur.
+
+
+ Ce serveur n'est pas configuré.
+
+
+ Petite plateforme : phase de combat — caisse lootable dans {0}.
+
+
+ Petite plateforme : pillée / dormante — réapparaît dans {0}.
+
+
+ Petite plateforme : caisse prête, en attente d'activation.
+
+
+ {0}
+
+
+ Aucun coéquipier ne correspond à « {0} ».
+
+
+ Aucun membre d'équipe.
+
+
+ Équipe ({0}) : {1}
+
+
+ jour
+
+
+ nuit
+
+
+ Heure : {0} — {1}
+
+
+ Bot réactivé.
+
+
+ Disponibilité : {0}
+
+
+ Wipe il y a {0}
+
+
+ Heure de wipe inconnue.
+
+
+ 🚢 Cargo Ship arrivé en {0}
+
+
+ Cargo Ship arrivé en {0}
+
+
+ 🚢 Cargo Ship parti ({0})
+
+
+ Cargo Ship parti ({0})
+
+
+ 🚁 Chinook apparu en {0}
+
+
+ Chinook apparu en {0}
+
+
+ 🚁 Hélicoptère de patrouille arrivé en {0}
+
+
+ Hélicoptère de patrouille arrivé en {0}
+
+
+ 🚁 Hélicoptère de patrouille parti ({0})
+
+
+ Hélicoptère de patrouille parti ({0})
+
+
+ 🛢️ Grande plateforme pétrolière activée — phase de combat, caisse bientôt lootable ({0})
+
+
+ Grande plateforme activée — phase de combat ({0})
+
+
+ 🛢️ Grande plateforme pétrolière — caisse LOOTABLE ({0})
+
+
+ Grande plateforme — caisse lootable ({0})
+
+
+ 🛢️ Grande plateforme pétrolière — caisse réapparue, réarmée ({0})
+
+
+ Grande plateforme — caisse réapparue ({0})
+
+
+ 🛢️ Petite plateforme pétrolière activée — phase de combat, caisse bientôt lootable ({0})
+
+
+ Petite plateforme activée — phase de combat ({0})
+
+
+ 🛢️ Petite plateforme pétrolière — caisse LOOTABLE ({0})
+
+
+ Petite plateforme — caisse lootable ({0})
+
+
+ 🛢️ Petite plateforme pétrolière — caisse réapparue, réarmée ({0})
+
+
+ Petite plateforme — caisse réapparue ({0})
+
+
+ Événement
+
+
+ Lister les coéquipiers qui sont AFK.
+
+
+ Afficher depuis combien de temps chaque coéquipier survit.
+
+
+ Bot
+
+
+ Contrôle
+
+
+ Serveur
+
+
+ Commandes slash
+
+
+ Équipe
+
+
+ Couper toute sortie du bot vers le jeu.
+
+
+ Lister les coéquipiers hors ligne.
+
+
+ Lister les coéquipiers en ligne.
+
+
+ Afficher la population du serveur.
+
+
+ Les préfixes sont configurés par serveur ; affichage du préfixe par défaut.
+
+
+ Afficher la distance à chaque coéquipier.
+
+
+ Afficher ce message d'aide.
+
+
+ Transférer le rôle de chef d'équipe en jeu.
+
+
+ Afficher la disponibilité du bot.
+
+
+ Afficher les identifiants Steam des coéquipiers.
+
+
+ Lister tous les membres de l'équipe.
+
+
+ Afficher l'heure en jeu.
+
+
+ Commandes
+
+
+ Réactiver la sortie du bot vers le jeu.
+
+
+ Afficher depuis combien de temps le bot fonctionne.
+
+
+ Afficher depuis combien de temps le serveur a été wipe.
+
+
+ Connectez votre compte Rust+ dans #configuration, puis appairez un serveur en jeu.
+
+
+ Serveurs enregistres : {0}
+
+
+ RustPlusBot
+
+
+ Impossible de promouvoir — le serveur est peut-être injoignable.
+
+
+ Aucun serveur n'est encore configuré.
+
+
+ Non connecté à ce serveur.
+
+
+ Aucun membre d'équipe à promouvoir.
+
+
+ Choisissez qui promouvoir chef d'équipe :
+
+
+ Choisissez un serveur :
+
+
+ {0} promu chef d'équipe.
+
+
+ Calques de la carte — activer/désactiver :
+
+
+ Grille
+
+
+ Marqueurs
+
+
+ Monuments
+
+
+ Joueurs
+
+
+ Plateformes
+
+
+ Marchand
+
+
+ 💤 {0} est AFK ({1})
+
+
+ 👋 {0} est de retour
+
+
+ {0} est de retour
+
+
+ {0} est AFK ({1})
+
+
+ 🟢 {0} s'est connecté
+
+
+ {0} s'est connecté
+
+
+ 💀 {0} est mort en {1}
+
+
+ {0} est mort en {1}
+
+
+ 💀 {0} est mort
+
+
+ {0} est mort
+
+
+ 🔴 {0} s'est déconnecté
+
+
+ {0} s'est déconnecté
+
+
+ ✨ {0} a réapparu en {1}
+
+
+ {0} a réapparu en {1}
+
+
+ Événement d'équipe
+
+
+ Adresse : {0}:{1}
+
+
+ —
+
+
+ Joueur actif
+
+
+ Joueurs en ligne
+
+
+ Supprimer le serveur
+
+
+ Connecté
+
+
+ Connexion…
+
+
+ État
+
+
+ Aucun identifiant valide
+
+
+ Serveur injoignable
+
+
+ Changer le joueur actif
+
+
+ Équipe
+
+
+ {0}/{1} en ligne · chef {2}
+
+
+ Configurez le bot pour ce serveur.
+
+
+ Langue
+
+
+ Parametres
+
+
+ Cliquez sur **Connecter le compte** ci-dessous et collez vos identifiants FCM Rust+. Appairez ensuite un serveur en jeu et ses salons apparaitront automatiquement.
+
+
+ Connecter le compte
+
+
+ Déconnecter le compte
+
+
+ Connectez votre compte Rust+
+
+
+ Éteindre
+
+
+ Allumer
+
+
+ Renommer
+
+
+ Stroboscope
+
+
+ Entité {0}
+
+
+ Accepter
+
+
+ Nouvel interrupteur connecté détecté ({0}). L'ajouter ?
+
+
+ Ignorer
+
+
+ Ignoré.
+
+
+ Nouvel interrupteur détecté
+
+
+ Nom de l'interrupteur
+
+
+ Renommer l'interrupteur
+
+
+ ⭘ ÉTEINT
+
+
+ ⚡ ALLUMÉ
+
+
+ ⚠️ Injoignable
+
+
+ L'interrupteur est injoignable pour le moment.
+
+
+ Disponibilité du bot : {0}
+
+
\ No newline at end of file
diff --git a/src/RustPlusBot.Localization/Strings.resx b/src/RustPlusBot.Localization/Strings.resx
new file mode 100644
index 0000000..8875517
--- /dev/null
+++ b/src/RustPlusBot.Localization/Strings.resx
@@ -0,0 +1,594 @@
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Ping @everyone: off
+
+
+ Ping @everyone: on
+
+
+ Relay to team chat: off
+
+
+ Relay to team chat: on
+
+
+ Rename
+
+
+ Entity {0}
+
+
+ Last triggered {0} ago
+
+
+ Never triggered
+
+
+ Accept
+
+
+ Detected a new Smart Alarm ({0}). Add it?
+
+
+ Dismiss
+
+
+ New alarm detected
+
+
+ Alarm name
+
+
+ Rename alarm
+
+
+ 🚨 Active
+
+
+ 🔔 Armed
+
+
+ ⚠️ Unreachable
+
+
+ 🚨 {0} triggered
+
+
+ RustPlusBot
+
+
+ alarms
+
+
+ events
+
+
+ info
+
+
+ information
+
+
+ map
+
+
+ settings
+
+
+ setup
+
+
+ switches
+
+
+ teamchat
+
+
+ {0} ({1})
+
+
+ Nobody is AFK.
+
+
+ AFK: {0}
+
+
+ {0} dead
+
+
+ {0} {1}
+
+
+ Alive: {0}
+
+
+ No cargo ship on the map.
+
+
+ Cargo Ship at {0} ({1} ago)
+
+
+ No chinook on the map.
+
+
+ Chinook at {0} ({1} ago)
+
+
+ cargo in {0}
+
+
+ cargo left {0}
+
+
+ chinook in {0}
+
+
+ heli in {0}
+
+
+ heli left {0}
+
+
+ No recent events.
+
+
+ Recent: {0}
+
+
+ No patrol helicopter on the map.
+
+
+ Patrol Helicopter at {0} ({1} ago)
+
+
+ Large Oil Rig: combat phase — crate lootable in {0}.
+
+
+ Large Oil Rig: looted / dormant — respawns in {0}.
+
+
+ Large Oil Rig: crate ready, waiting for activation.
+
+
+ Bot muted.
+
+
+ Not connected to the server.
+
+
+ Everyone is online.
+
+
+ Offline ({0}): {1}
+
+
+ No one is online.
+
+
+ Online ({0}): {1}
+
+
+ Pop: {0}/{1} ({2} queued)
+
+
+ No teammates nearby.
+
+
+ {0} {1}m
+
+
+ Prox: {0}
+
+
+ Can't locate you.
+
+
+ No server is set up yet.
+
+
+ Multiple servers are set up — choose one with the server option.
+
+
+ That server isn't set up.
+
+
+ Small Oil Rig: combat phase — crate lootable in {0}.
+
+
+ Small Oil Rig: looted / dormant — respawns in {0}.
+
+
+ Small Oil Rig: crate ready, waiting for activation.
+
+
+ {0}
+
+
+ No teammate matches '{0}'.
+
+
+ No team members.
+
+
+ Team ({0}): {1}
+
+
+ day
+
+
+ night
+
+
+ Time: {0} — {1}
+
+
+ Bot unmuted.
+
+
+ Uptime: {0}
+
+
+ Wiped {0} ago
+
+
+ Wipe time is unknown.
+
+
+ 🚢 Cargo Ship entered at {0}
+
+
+ Cargo Ship entered at {0}
+
+
+ 🚢 Cargo Ship left ({0})
+
+
+ Cargo Ship left ({0})
+
+
+ 🚁 Chinook spawned at {0}
+
+
+ Chinook spawned at {0}
+
+
+ 🚁 Patrol Helicopter entered at {0}
+
+
+ Patrol Helicopter entered at {0}
+
+
+ 🚁 Patrol Helicopter left ({0})
+
+
+ Patrol Helicopter left ({0})
+
+
+ 🛢️ Large Oil Rig activated — combat phase, crate lootable soon ({0})
+
+
+ Large Oil Rig activated — combat phase ({0})
+
+
+ 🛢️ Large Oil Rig — crate is now LOOTABLE ({0})
+
+
+ Large Oil Rig — crate is now lootable ({0})
+
+
+ 🛢️ Large Oil Rig — crate respawned, armed again ({0})
+
+
+ Large Oil Rig — crate respawned ({0})
+
+
+ 🛢️ Small Oil Rig activated — combat phase, crate lootable soon ({0})
+
+
+ Small Oil Rig activated — combat phase ({0})
+
+
+ 🛢️ Small Oil Rig — crate is now LOOTABLE ({0})
+
+
+ Small Oil Rig — crate is now lootable ({0})
+
+
+ 🛢️ Small Oil Rig — crate respawned, armed again ({0})
+
+
+ Small Oil Rig — crate respawned ({0})
+
+
+ Live event
+
+
+ List teammates who are AFK.
+
+
+ Show how long each teammate has survived.
+
+
+ Bot
+
+
+ Control
+
+
+ Server
+
+
+ Slash commands
+
+
+ Team
+
+
+ Silence all bot output to the game.
+
+
+ List offline teammates.
+
+
+ List online teammates.
+
+
+ Show the server population.
+
+
+ Prefixes are configured per server; showing the default.
+
+
+ Show distance to each teammate.
+
+
+ Show this help message.
+
+
+ Transfer in-game team leadership.
+
+
+ Show the bot's uptime.
+
+
+ Show teammates' Steam IDs.
+
+
+ List all team members.
+
+
+ Show the in-game time.
+
+
+ Commands
+
+
+ Resume bot output to the game.
+
+
+ Show how long the bot has been running.
+
+
+ Show how long ago the server wiped.
+
+
+ Connect your Rust+ account in #setup, then pair a server in-game to begin.
+
+
+ Servers registered: {0}
+
+
+ RustPlusBot
+
+
+ Couldn't promote — the server may be unreachable.
+
+
+ No server is set up yet.
+
+
+ Not connected to this server.
+
+
+ No team members to promote.
+
+
+ Choose who to promote to team leader:
+
+
+ Choose a server:
+
+
+ Promoted {0} to team leader.
+
+
+ Map layers — toggle to show/hide:
+
+
+ Grid
+
+
+ Markers
+
+
+ Monuments
+
+
+ Players
+
+
+ Rigs
+
+
+ Vendor
+
+
+ 💤 {0} is AFK ({1})
+
+
+ 👋 {0} is back
+
+
+ {0} is back
+
+
+ {0} is AFK ({1})
+
+
+ 🟢 {0} connected
+
+
+ {0} connected
+
+
+ 💀 {0} died at {1}
+
+
+ {0} died at {1}
+
+
+ 💀 {0} died
+
+
+ {0} died
+
+
+ 🔴 {0} disconnected
+
+
+ {0} disconnected
+
+
+ ✨ {0} respawned at {1}
+
+
+ {0} respawned at {1}
+
+
+ Team event
+
+
+ Endpoint: {0}:{1}
+
+
+ —
+
+
+ Active player
+
+
+ Players online
+
+
+ Remove server
+
+
+ Connected
+
+
+ Connecting…
+
+
+ Status
+
+
+ No working credentials
+
+
+ Server unreachable
+
+
+ Switch active player
+
+
+ Team
+
+
+ {0}/{1} online · leader {2}
+
+
+ Configure the bot for this server.
+
+
+ Language
+
+
+ Settings
+
+
+ Click **Connect account** below and paste your Rust+ FCM credentials. Then pair a server in-game and its channels appear automatically.
+
+
+ Connect account
+
+
+ Disconnect account
+
+
+ Connect your Rust+ account
+
+
+ Turn off
+
+
+ Turn on
+
+
+ Rename
+
+
+ Strobe
+
+
+ Entity {0}
+
+
+ Accept
+
+
+ Detected a new Smart Switch ({0}). Add it?
+
+
+ Dismiss
+
+
+ Dismissed.
+
+
+ New switch detected
+
+
+ Switch name
+
+
+ Rename switch
+
+
+ ⭘ OFF
+
+
+ ⚡ ON
+
+
+ ⚠️ Unreachable
+
+
+ Switch is unreachable right now.
+
+
+ Bot uptime: {0}
+
+
\ No newline at end of file
diff --git a/tests/RustPlusBot.Features.Alarms.Tests/AlarmEmbedRendererTests.cs b/tests/RustPlusBot.Features.Alarms.Tests/AlarmEmbedRendererTests.cs
index a1a666a..8b3b971 100644
--- a/tests/RustPlusBot.Features.Alarms.Tests/AlarmEmbedRendererTests.cs
+++ b/tests/RustPlusBot.Features.Alarms.Tests/AlarmEmbedRendererTests.cs
@@ -3,6 +3,7 @@
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Domain.Alarms;
using RustPlusBot.Features.Alarms.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Alarms.Tests;
@@ -14,7 +15,7 @@ private static AlarmEmbedRenderer Create(DateTimeOffset? now = null)
{
var clock = Substitute.For();
clock.UtcNow.Returns(now ?? _fixedNow);
- var localizer = new AlarmLocalizer(AlarmLocalizationCatalog.Default);
+ var localizer = new ResxLocalizer();
return new AlarmEmbedRenderer(localizer, clock);
}
diff --git a/tests/RustPlusBot.Features.Alarms.Tests/AlarmLocalizationCatalogTests.cs b/tests/RustPlusBot.Features.Alarms.Tests/AlarmLocalizationCatalogTests.cs
deleted file mode 100644
index 2736d0c..0000000
--- a/tests/RustPlusBot.Features.Alarms.Tests/AlarmLocalizationCatalogTests.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using RustPlusBot.Features.Alarms.Rendering;
-
-namespace RustPlusBot.Features.Alarms.Tests;
-
-public sealed class AlarmLocalizationCatalogTests
-{
- private static readonly string[] RequiredKeys =
- [
- "alarm.status.armed",
- "alarm.status.active",
- "alarm.status.unreachable",
- "alarm.button.ping.on",
- "alarm.button.ping.off",
- "alarm.button.relay.on",
- "alarm.button.relay.off",
- "alarm.button.rename",
- "alarm.embed.footer",
- "alarm.embed.nevertriggered",
- "alarm.embed.lasttriggered",
- "alarm.prompt.title",
- "alarm.prompt.body",
- "alarm.prompt.accept",
- "alarm.prompt.dismiss",
- "alarm.rename.modal.title",
- "alarm.rename.input.label",
- "alarm.triggered.teamchat",
- ];
-
- [Theory]
- [InlineData("en")]
- [InlineData("fr")]
- public void Catalog_contains_all_required_keys(string culture)
- {
- var map = AlarmLocalizationCatalog.Default[culture];
- foreach (var key in RequiredKeys)
- {
- Assert.True(map.ContainsKey(key), $"Missing key '{key}' for culture '{culture}'.");
- }
- }
-
- [Fact]
- public void En_and_fr_have_identical_key_sets()
- {
- var enKeys = new SortedSet(AlarmLocalizationCatalog.Default["en"].Keys);
- var frKeys = new SortedSet(AlarmLocalizationCatalog.Default["fr"].Keys);
- Assert.Equal(enKeys, frKeys);
- }
-}
diff --git a/tests/RustPlusBot.Features.Alarms.Tests/AlarmPairingCoordinatorTests.cs b/tests/RustPlusBot.Features.Alarms.Tests/AlarmPairingCoordinatorTests.cs
index 2353312..f982c10 100644
--- a/tests/RustPlusBot.Features.Alarms.Tests/AlarmPairingCoordinatorTests.cs
+++ b/tests/RustPlusBot.Features.Alarms.Tests/AlarmPairingCoordinatorTests.cs
@@ -7,6 +7,7 @@
using RustPlusBot.Features.Alarms.Posting;
using RustPlusBot.Features.Alarms.Rendering;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Alarms;
using RustPlusBot.Persistence.Workspace;
@@ -37,7 +38,7 @@ private static Harness Create()
var clock = Substitute.For();
clock.UtcNow.Returns(new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero));
- var localizer = new AlarmLocalizer(AlarmLocalizationCatalog.Default);
+ var localizer = new ResxLocalizer();
var renderer = new AlarmEmbedRenderer(localizer, clock);
var coordinator = new AlarmPairingCoordinator(scopeFactory, locator, poster, renderer);
diff --git a/tests/RustPlusBot.Features.Alarms.Tests/AlarmRefresherTests.cs b/tests/RustPlusBot.Features.Alarms.Tests/AlarmRefresherTests.cs
index 7220cda..99bafe7 100644
--- a/tests/RustPlusBot.Features.Alarms.Tests/AlarmRefresherTests.cs
+++ b/tests/RustPlusBot.Features.Alarms.Tests/AlarmRefresherTests.cs
@@ -6,6 +6,7 @@
using RustPlusBot.Features.Alarms.Relaying;
using RustPlusBot.Features.Alarms.Rendering;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Alarms;
using RustPlusBot.Persistence.Workspace;
@@ -38,7 +39,7 @@ private static Harness Create(SmartAlarm? alarm = null, ulong? channelId = 777UL
var clock = Substitute.For();
clock.UtcNow.Returns(_fixedNow);
- var localizer = new AlarmLocalizer(AlarmLocalizationCatalog.Default);
+ var localizer = new ResxLocalizer();
var renderer = new AlarmEmbedRenderer(localizer, clock);
if (alarm is not null)
diff --git a/tests/RustPlusBot.Features.Alarms.Tests/AlarmRegistrationTests.cs b/tests/RustPlusBot.Features.Alarms.Tests/AlarmRegistrationTests.cs
index 4bd0b39..8f47294 100644
--- a/tests/RustPlusBot.Features.Alarms.Tests/AlarmRegistrationTests.cs
+++ b/tests/RustPlusBot.Features.Alarms.Tests/AlarmRegistrationTests.cs
@@ -11,6 +11,7 @@
using RustPlusBot.Features.Alarms.Rendering;
using RustPlusBot.Features.Connections.Listening;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Alarms;
using RustPlusBot.Persistence.Connections;
using RustPlusBot.Persistence.Workspace;
@@ -27,7 +28,7 @@ public void AddAlarms_registers_core_services()
var services = new ServiceCollection();
services.AddAlarms();
- Assert.Contains(services, d => d.ServiceType == typeof(IAlarmLocalizer));
+ Assert.Contains(services, d => d.ServiceType == typeof(ILocalizer));
Assert.Contains(services, d => d.ServiceType == typeof(AlarmEmbedRenderer));
Assert.Contains(services, d => d.ServiceType == typeof(IAlarmChannelPoster));
Assert.Contains(services, d => d.ServiceType == typeof(IAlarmRefresher));
@@ -64,7 +65,7 @@ public void AddAlarms_resolves_without_captive_dependency_errors()
using var provider = services.BuildServiceProvider(validateScopes: true);
- var localizer = provider.GetRequiredService();
+ var localizer = provider.GetRequiredService();
var renderer = provider.GetRequiredService();
var poster = provider.GetRequiredService();
var refresher = provider.GetRequiredService();
diff --git a/tests/RustPlusBot.Features.Alarms.Tests/AlarmStateRelayTests.cs b/tests/RustPlusBot.Features.Alarms.Tests/AlarmStateRelayTests.cs
index cb6ec22..7f073e4 100644
--- a/tests/RustPlusBot.Features.Alarms.Tests/AlarmStateRelayTests.cs
+++ b/tests/RustPlusBot.Features.Alarms.Tests/AlarmStateRelayTests.cs
@@ -11,6 +11,7 @@
using RustPlusBot.Features.Alarms.Rendering;
using RustPlusBot.Features.Connections.Listening;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Alarms;
using RustPlusBot.Persistence.Connections;
using RustPlusBot.Persistence.Workspace;
@@ -47,7 +48,7 @@ private static Harness Create(SmartAlarm? alarm = null, ulong? channelId = 777UL
.SendAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any())
.Returns(TeamChatSendResult.Sent);
- var alarmLocalizer = new AlarmLocalizer(AlarmLocalizationCatalog.Default);
+ var alarmLocalizer = new ResxLocalizer();
var clock = Substitute.For();
clock.UtcNow.Returns(_fixedNow);
diff --git a/tests/RustPlusBot.Features.Commands.Tests/AfkCommandHandlerTests.cs b/tests/RustPlusBot.Features.Commands.Tests/AfkCommandHandlerTests.cs
index e70c041..24ffcc1 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/AfkCommandHandlerTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/AfkCommandHandlerTests.cs
@@ -1,15 +1,15 @@
using NSubstitute;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Handlers;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Connections.Listening;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests;
public sealed class AfkCommandHandlerTests
{
private readonly IAfkState _afk = Substitute.For();
- private readonly ICommandLocalizer _localizer = new CommandLocalizer(CommandLocalizationCatalog.Default);
+ private readonly ILocalizer _localizer = new ResxLocalizer();
private static CommandContext Ctx() => new(1, Guid.NewGuid(), "en", 99, "Caller", []);
diff --git a/tests/RustPlusBot.Features.Commands.Tests/CommandRegistrationTests.cs b/tests/RustPlusBot.Features.Commands.Tests/CommandRegistrationTests.cs
index c2dc2b8..bc744d3 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/CommandRegistrationTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/CommandRegistrationTests.cs
@@ -7,10 +7,10 @@
using RustPlusBot.Discord;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Hosting;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Commands.Servers;
using RustPlusBot.Features.Connections.Listening;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Commands;
using RustPlusBot.Persistence.Servers;
using RustPlusBot.Persistence.Workspace;
@@ -44,7 +44,7 @@ public void Dispatcher_and_handlers_resolve()
// The singletons AddCommands wires must each resolve (guards against an accidentally dropped registration).
Assert.NotNull(provider.GetRequiredService());
Assert.NotNull(provider.GetRequiredService());
- Assert.NotNull(provider.GetRequiredService());
+ Assert.NotNull(provider.GetRequiredService());
using var scope = provider.CreateScope();
Assert.NotNull(scope.ServiceProvider.GetRequiredService());
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Handlers/EventHandlersTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Handlers/EventHandlersTests.cs
index 89756bb..e19ae1e 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Handlers/EventHandlersTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Handlers/EventHandlersTests.cs
@@ -3,9 +3,9 @@
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Handlers;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Events.Classifying;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests.Handlers;
@@ -15,11 +15,11 @@ public sealed class EventHandlersTests
private static readonly Guid Server = Guid.NewGuid();
private static readonly DateTimeOffset Now = new(2026, 6, 17, 12, 5, 0, TimeSpan.Zero);
- private static (IClock Clock, ICommandLocalizer Loc) Deps()
+ private static (IClock Clock, ILocalizer Loc) Deps()
{
var clock = Substitute.For();
clock.UtcNow.Returns(Now);
- return (clock, new CommandLocalizer(CommandLocalizationCatalog.Default));
+ return (clock, new ResxLocalizer());
}
private static CommandContext Ctx() => new(Guild, Server, "en", 0UL, string.Empty, []);
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Handlers/MuteHandlersTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Handlers/MuteHandlersTests.cs
index b095db3..781f530 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Handlers/MuteHandlersTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Handlers/MuteHandlersTests.cs
@@ -1,14 +1,14 @@
using NSubstitute;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Handlers;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Commands;
namespace RustPlusBot.Features.Commands.Tests.Handlers;
public sealed class MuteHandlersTests
{
- private static readonly ICommandLocalizer Loc = new CommandLocalizer(CommandLocalizationCatalog.Default);
+ private static readonly ILocalizer Loc = new ResxLocalizer();
private static CommandContext Ctx(string culture = "en") =>
new(1, Guid.NewGuid(), culture, 7, "alice", []);
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Handlers/QueryHandlersTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Handlers/QueryHandlersTests.cs
index d6b9ff1..23d3a69 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Handlers/QueryHandlersTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Handlers/QueryHandlersTests.cs
@@ -4,13 +4,13 @@
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Handlers;
using RustPlusBot.Features.Commands.Hosting;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests.Handlers;
public sealed class QueryHandlersTests
{
- private static readonly ICommandLocalizer Loc = new CommandLocalizer(CommandLocalizationCatalog.Default);
+ private static readonly ILocalizer Loc = new ResxLocalizer();
private static CommandContext Ctx() => new(1, Guid.NewGuid(), "en", 7, "alice", []);
[Fact]
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Handlers/RigCommandHandlersTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Handlers/RigCommandHandlersTests.cs
index 998703f..2ea1b03 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Handlers/RigCommandHandlersTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Handlers/RigCommandHandlersTests.cs
@@ -2,8 +2,8 @@
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Handlers;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Events.State;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests.Handlers;
@@ -14,8 +14,7 @@ public sealed class RigCommandHandlersTests
private static CommandContext Ctx() => new(Guild, Server, "en", 0UL, string.Empty, []);
- private static ICommandLocalizer RealLocalizer() =>
- new CommandLocalizer(CommandLocalizationCatalog.Default);
+ private static ILocalizer RealLocalizer() => new ResxLocalizer();
[Fact]
public async Task Small_reports_online_when_untracked()
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Handlers/TeamIntelHandlersTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Handlers/TeamIntelHandlersTests.cs
index 84afbd0..a13731b 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Handlers/TeamIntelHandlersTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Handlers/TeamIntelHandlersTests.cs
@@ -3,13 +3,13 @@
using RustPlusBot.Abstractions.Time;
using RustPlusBot.Features.Commands.Dispatching;
using RustPlusBot.Features.Commands.Handlers;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests.Handlers;
public sealed class TeamIntelHandlersTests
{
- private static readonly ICommandLocalizer Loc = new CommandLocalizer(CommandLocalizationCatalog.Default);
+ private static readonly ILocalizer Loc = new ResxLocalizer();
private static readonly Guid ServerId = Guid.NewGuid();
private static CommandContext Ctx(params string[] args) =>
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Help/CommandHelpCatalogTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Help/CommandHelpCatalogTests.cs
index e19fa6c..ddedcd8 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Help/CommandHelpCatalogTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Help/CommandHelpCatalogTests.cs
@@ -1,5 +1,5 @@
using RustPlusBot.Features.Commands.Help;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests.Help;
@@ -35,7 +35,7 @@ public void InGameCatalogHasNoEntryWithoutAHandler()
[Fact]
public void EveryDescriptionKeyResolvesInEnglishAndFrench()
{
- var loc = new CommandLocalizer(CommandLocalizationCatalog.Default);
+ var loc = new ResxLocalizer();
foreach (var entry in CommandHelpCatalog.InGame.Concat(CommandHelpCatalog.Slash))
{
Assert.NotEqual(entry.DescriptionKey, loc.Get(entry.DescriptionKey, "en"));
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Help/HelpEmbedRendererTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Help/HelpEmbedRendererTests.cs
index 3b88a8c..e917ada 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Help/HelpEmbedRendererTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Help/HelpEmbedRendererTests.cs
@@ -1,12 +1,11 @@
using RustPlusBot.Features.Commands.Help;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests.Help;
public sealed class HelpEmbedRendererTests
{
- private static readonly HelpEmbedRenderer Renderer =
- new(new CommandLocalizer(CommandLocalizationCatalog.Default));
+ private static readonly HelpEmbedRenderer Renderer = new(new ResxLocalizer());
[Fact]
public void Render_PrefixesInGameCommands()
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Leader/LeaderServiceTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Leader/LeaderServiceTests.cs
index e85a381..675ae11 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Leader/LeaderServiceTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Leader/LeaderServiceTests.cs
@@ -1,13 +1,13 @@
using NSubstitute;
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Commands.Leader;
-using RustPlusBot.Features.Commands.Localization;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Commands.Tests.Leader;
public sealed class LeaderServiceTests
{
- private static readonly CommandLocalizer Loc = new(CommandLocalizationCatalog.Default);
+ private static readonly ResxLocalizer Loc = new();
private static readonly Guid Server = Guid.NewGuid();
[Fact]
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Localization/CommandLocalizerTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Localization/CommandLocalizerTests.cs
deleted file mode 100644
index 75d542c..0000000
--- a/tests/RustPlusBot.Features.Commands.Tests/Localization/CommandLocalizerTests.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using RustPlusBot.Features.Commands.Localization;
-
-namespace RustPlusBot.Features.Commands.Tests.Localization;
-
-public sealed class CommandLocalizerTests
-{
- private static readonly CommandLocalizer Sut = new(CommandLocalizationCatalog.Default);
-
- [Fact]
- public void ReturnsFrench_WhenCultureFr() =>
- Assert.Equal("Bot mis en sourdine.", Sut.Get("command.mute.done", "fr"));
-
- [Fact]
- public void FallsBackToEnglish_WhenCultureUnknown() =>
- Assert.Equal("Bot muted.", Sut.Get("command.mute.done", "xx"));
-
- [Fact]
- public void FormatsArgs() =>
- Assert.Equal("Pop: 5/100 (2 queued)", Sut.Get("command.pop.ok", "en", 5, 100, 2));
-
- [Fact]
- public void NormalizesRegion() =>
- Assert.Equal("Bot mis en sourdine.", Sut.Get("command.mute.done", "fr-FR"));
-}
diff --git a/tests/RustPlusBot.Features.Commands.Tests/Servers/ServerResolverTests.cs b/tests/RustPlusBot.Features.Commands.Tests/Servers/ServerResolverTests.cs
index e8c0afa..188c25f 100644
--- a/tests/RustPlusBot.Features.Commands.Tests/Servers/ServerResolverTests.cs
+++ b/tests/RustPlusBot.Features.Commands.Tests/Servers/ServerResolverTests.cs
@@ -1,14 +1,14 @@
using NSubstitute;
using RustPlusBot.Domain.Servers;
-using RustPlusBot.Features.Commands.Localization;
using RustPlusBot.Features.Commands.Servers;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Servers;
namespace RustPlusBot.Features.Commands.Tests.Servers;
public sealed class ServerResolverTests
{
- private static readonly CommandLocalizer Loc = new(CommandLocalizationCatalog.Default);
+ private static readonly ResxLocalizer Loc = new();
private static RustServer Server(Guid id, string name) => new()
{
diff --git a/tests/RustPlusBot.Features.Events.Tests/Relaying/EventRelayTests.cs b/tests/RustPlusBot.Features.Events.Tests/Relaying/EventRelayTests.cs
index c2b39ba..70f5310 100644
--- a/tests/RustPlusBot.Features.Events.Tests/Relaying/EventRelayTests.cs
+++ b/tests/RustPlusBot.Features.Events.Tests/Relaying/EventRelayTests.cs
@@ -13,6 +13,7 @@
using RustPlusBot.Features.Events.Rendering;
using RustPlusBot.Features.Events.State;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Workspace;
namespace RustPlusBot.Features.Events.Tests.Relaying;
@@ -44,7 +45,7 @@ private static (EventRelay Relay, EventStateStore Store, IEventChannelPoster Pos
var sender = Substitute.For();
var rigStore = new RigStateStore(clock, Options.Create(new ConnectionOptions()));
- var renderer = new EventEmbedRenderer(new EventLocalizer(EventLocalizationCatalog.Default));
+ var renderer = new EventEmbedRenderer(new ResxLocalizer());
var relay = new EventRelay(
new MarkerEventClassifier(clock),
diff --git a/tests/RustPlusBot.Features.Events.Tests/Rendering/EventEmbedRendererTests.cs b/tests/RustPlusBot.Features.Events.Tests/Rendering/EventEmbedRendererTests.cs
index ce3e982..00d8ccc 100644
--- a/tests/RustPlusBot.Features.Events.Tests/Rendering/EventEmbedRendererTests.cs
+++ b/tests/RustPlusBot.Features.Events.Tests/Rendering/EventEmbedRendererTests.cs
@@ -1,6 +1,7 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Features.Events.Classifying;
using RustPlusBot.Features.Events.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Events.Tests.Rendering;
@@ -9,7 +10,7 @@ public sealed class EventEmbedRendererTests
private static readonly DateTimeOffset Now = new(2026, 6, 17, 12, 0, 0, TimeSpan.Zero);
private static EventEmbedRenderer Build() =>
- new(new EventLocalizer(EventLocalizationCatalog.Default));
+ new(new ResxLocalizer());
[Fact]
public void Cargo_entered_renders_english_with_grid()
diff --git a/tests/RustPlusBot.Features.Events.Tests/Rendering/EventLocalizerTests.cs b/tests/RustPlusBot.Features.Events.Tests/Rendering/EventLocalizerTests.cs
deleted file mode 100644
index cc3bd7a..0000000
--- a/tests/RustPlusBot.Features.Events.Tests/Rendering/EventLocalizerTests.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using RustPlusBot.Features.Events.Rendering;
-
-namespace RustPlusBot.Features.Events.Tests.Rendering;
-
-public sealed class EventLocalizerTests
-{
- private static readonly EventLocalizer Sut = new(EventLocalizationCatalog.Default);
-
- [Fact]
- public void ReturnsFrench_WhenCultureFr() =>
- Assert.Equal("🚁 Chinook apparu en {0}", Sut.Get("event.chinook.spawned", "fr"));
-
- [Fact]
- public void ReturnsEnglish_WhenCultureEn() =>
- Assert.Equal("🚁 Chinook spawned at {0}", Sut.Get("event.chinook.spawned", "en"));
-
- [Fact]
- public void FallsBackToEnglish_WhenCultureUnknown() =>
- Assert.Equal("🚁 Chinook spawned at {0}", Sut.Get("event.chinook.spawned", "xx"));
-
- [Fact]
- public void ReturnsKey_WhenKeyUnknown() =>
- Assert.Equal("unknown.key", Sut.Get("unknown.key", "en"));
-
- [Fact]
- public void NormalizesRegion() =>
- Assert.Equal("🚁 Chinook apparu en {0}", Sut.Get("event.chinook.spawned", "fr-FR"));
-}
diff --git a/tests/RustPlusBot.Features.Events.Tests/Rendering/RigRenderingTests.cs b/tests/RustPlusBot.Features.Events.Tests/Rendering/RigRenderingTests.cs
index f71b51b..feafdf1 100644
--- a/tests/RustPlusBot.Features.Events.Tests/Rendering/RigRenderingTests.cs
+++ b/tests/RustPlusBot.Features.Events.Tests/Rendering/RigRenderingTests.cs
@@ -1,13 +1,14 @@
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Events.Classifying;
using RustPlusBot.Features.Events.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Events.Tests.Rendering;
public sealed class RigRenderingTests
{
private static EventEmbedRenderer Renderer() =>
- new(new EventLocalizer(EventLocalizationCatalog.Default));
+ new(new ResxLocalizer());
[Theory]
[InlineData(RigKind.Small, RigEventKind.Activated, "en")]
diff --git a/tests/RustPlusBot.Features.Players.Tests/PlayerEventEndToEndTests.cs b/tests/RustPlusBot.Features.Players.Tests/PlayerEventEndToEndTests.cs
index 3069465..af2dba1 100644
--- a/tests/RustPlusBot.Features.Players.Tests/PlayerEventEndToEndTests.cs
+++ b/tests/RustPlusBot.Features.Players.Tests/PlayerEventEndToEndTests.cs
@@ -1,6 +1,7 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Players.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Players.Tests;
@@ -9,7 +10,7 @@ public sealed class PlayerEventEndToEndTests
[Fact]
public void All_transition_kinds_render_without_throwing()
{
- var renderer = new PlayerEventRenderer(new PlayerLocalizer(PlayerLocalizationCatalog.Default));
+ var renderer = new PlayerEventRenderer(new ResxLocalizer());
var dims = new MapDimensions(3000, 3000, 0);
foreach (var kind in Enum.GetValues())
{
diff --git a/tests/RustPlusBot.Features.Players.Tests/PlayerEventRelayTests.cs b/tests/RustPlusBot.Features.Players.Tests/PlayerEventRelayTests.cs
index ec9ddb2..7cf88a1 100644
--- a/tests/RustPlusBot.Features.Players.Tests/PlayerEventRelayTests.cs
+++ b/tests/RustPlusBot.Features.Players.Tests/PlayerEventRelayTests.cs
@@ -7,6 +7,7 @@
using RustPlusBot.Features.Players.Relaying;
using RustPlusBot.Features.Players.Rendering;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Workspace;
namespace RustPlusBot.Features.Players.Tests;
@@ -23,7 +24,7 @@ private PlayerEventRelay BuildRelay()
var scopeFactory = BuildScopeFactory(_workspace);
_workspace.GetCultureAsync(Arg.Any(), Arg.Any()).Returns("en");
return new PlayerEventRelay(
- new PlayerEventRenderer(new PlayerLocalizer(PlayerLocalizationCatalog.Default)),
+ new PlayerEventRenderer(new ResxLocalizer()),
_locator, _poster, _sender, scopeFactory);
}
diff --git a/tests/RustPlusBot.Features.Players.Tests/PlayerEventRendererTests.cs b/tests/RustPlusBot.Features.Players.Tests/PlayerEventRendererTests.cs
index 71266a0..1b8e3fd 100644
--- a/tests/RustPlusBot.Features.Players.Tests/PlayerEventRendererTests.cs
+++ b/tests/RustPlusBot.Features.Players.Tests/PlayerEventRendererTests.cs
@@ -1,12 +1,13 @@
using RustPlusBot.Abstractions.Connections;
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Players.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Players.Tests;
public sealed class PlayerEventRendererTests
{
- private static readonly PlayerEventRenderer Renderer = new(new PlayerLocalizer(PlayerLocalizationCatalog.Default));
+ private static readonly PlayerEventRenderer Renderer = new(new ResxLocalizer());
private static readonly MapDimensions Dims = new(3000, 3000, 0);
[Fact]
diff --git a/tests/RustPlusBot.Features.Players.Tests/PlayerLocalizationCatalogTests.cs b/tests/RustPlusBot.Features.Players.Tests/PlayerLocalizationCatalogTests.cs
deleted file mode 100644
index e78ee5c..0000000
--- a/tests/RustPlusBot.Features.Players.Tests/PlayerLocalizationCatalogTests.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using RustPlusBot.Features.Players.Rendering;
-
-namespace RustPlusBot.Features.Players.Tests;
-
-public sealed class PlayerLocalizationCatalogTests
-{
- private static readonly string[] Keys =
- [
- "player.title",
- "player.connect", "player.connect.line",
- "player.disconnect", "player.disconnect.line",
- "player.death", "player.death.line",
- "player.death.unknown", "player.death.unknown.line",
- "player.respawn", "player.respawn.line",
- "player.afk", "player.afk.line",
- "player.afk.back", "player.afk.back.line",
- ];
-
- [Theory]
- [InlineData("en")]
- [InlineData("fr")]
- public void Every_key_present_for_culture(string culture)
- {
- var catalog = PlayerLocalizationCatalog.Default;
- Assert.True(catalog.Strings.TryGetValue(culture, out var map));
- foreach (var key in Keys)
- {
- Assert.True(map!.ContainsKey(key), $"missing {key} for {culture}");
- }
- }
-
- [Fact]
- public void Localizer_formats_with_args_and_falls_back_to_english()
- {
- var localizer = new PlayerLocalizer(PlayerLocalizationCatalog.Default);
- Assert.Contains("Bob", localizer.Get("player.connect", "en", "Bob"), StringComparison.Ordinal);
- // Unknown culture falls back to English string (not the raw key).
- Assert.Contains("Bob", localizer.Get("player.connect", "de", "Bob"), StringComparison.Ordinal);
- }
-}
diff --git a/tests/RustPlusBot.Features.Switches.Tests/SwitchEmbedRendererTests.cs b/tests/RustPlusBot.Features.Switches.Tests/SwitchEmbedRendererTests.cs
index 9e23bfd..51e36ad 100644
--- a/tests/RustPlusBot.Features.Switches.Tests/SwitchEmbedRendererTests.cs
+++ b/tests/RustPlusBot.Features.Switches.Tests/SwitchEmbedRendererTests.cs
@@ -1,13 +1,14 @@
using Discord;
using RustPlusBot.Domain.Switches;
using RustPlusBot.Features.Switches.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Switches.Tests;
public sealed class SwitchEmbedRendererTests
{
private static SwitchEmbedRenderer Create() =>
- new(new SwitchLocalizer(SwitchLocalizationCatalog.Default));
+ new(new ResxLocalizer());
private static SmartSwitch Sample(string name = "Front gate", bool lastActive = false) => new()
{
diff --git a/tests/RustPlusBot.Features.Switches.Tests/SwitchLocalizationCatalogTests.cs b/tests/RustPlusBot.Features.Switches.Tests/SwitchLocalizationCatalogTests.cs
deleted file mode 100644
index 65976b6..0000000
--- a/tests/RustPlusBot.Features.Switches.Tests/SwitchLocalizationCatalogTests.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using RustPlusBot.Features.Switches.Rendering;
-
-namespace RustPlusBot.Features.Switches.Tests;
-
-public sealed class SwitchLocalizationCatalogTests
-{
- [Theory]
- [InlineData("en")]
- [InlineData("fr")]
- public void Catalog_contains_all_required_keys(string culture)
- {
- var map = SwitchLocalizationCatalog.Default.Strings[culture];
- foreach (var key in new[]
- {
- "switch.status.on", "switch.status.off", "switch.status.unreachable", "switch.button.on",
- "switch.button.off", "switch.button.strobe", "switch.button.rename", "switch.prompt.title",
- "switch.prompt.body", "switch.prompt.accept", "switch.prompt.dismiss", "switch.rename.modal.title",
- "switch.rename.input.label", "switch.unreachable.ephemeral",
- })
- {
- Assert.True(map.ContainsKey(key), $"Missing key '{key}' for culture '{culture}'.");
- }
- }
-
- [Fact]
- public void Localizer_falls_back_to_english()
- {
- var localizer = new SwitchLocalizer(SwitchLocalizationCatalog.Default);
- Assert.Equal(
- SwitchLocalizationCatalog.Default.Strings["en"]["switch.status.on"],
- localizer.Get("switch.status.on", "de"));
- }
-}
diff --git a/tests/RustPlusBot.Features.Switches.Tests/SwitchPairingCoordinatorTests.cs b/tests/RustPlusBot.Features.Switches.Tests/SwitchPairingCoordinatorTests.cs
index 584e10c..1707d47 100644
--- a/tests/RustPlusBot.Features.Switches.Tests/SwitchPairingCoordinatorTests.cs
+++ b/tests/RustPlusBot.Features.Switches.Tests/SwitchPairingCoordinatorTests.cs
@@ -5,6 +5,7 @@
using RustPlusBot.Features.Switches.Posting;
using RustPlusBot.Features.Switches.Rendering;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Switches;
using RustPlusBot.Persistence.Workspace;
@@ -33,7 +34,7 @@ private static Harness Create()
Arg.Any(), Arg.Any())
.Returns(900UL);
- var renderer = new SwitchEmbedRenderer(new SwitchLocalizer(SwitchLocalizationCatalog.Default));
+ var renderer = new SwitchEmbedRenderer(new ResxLocalizer());
var coordinator = new SwitchPairingCoordinator(scopeFactory, locator, poster, renderer);
return new Harness(coordinator, store, poster, locator);
}
diff --git a/tests/RustPlusBot.Features.Switches.Tests/SwitchRegistrationTests.cs b/tests/RustPlusBot.Features.Switches.Tests/SwitchRegistrationTests.cs
index 464f42b..fd05ee5 100644
--- a/tests/RustPlusBot.Features.Switches.Tests/SwitchRegistrationTests.cs
+++ b/tests/RustPlusBot.Features.Switches.Tests/SwitchRegistrationTests.cs
@@ -3,6 +3,7 @@
using RustPlusBot.Features.Switches.Pairing;
using RustPlusBot.Features.Switches.Relaying;
using RustPlusBot.Features.Switches.Rendering;
+using RustPlusBot.Localization;
namespace RustPlusBot.Features.Switches.Tests;
@@ -17,6 +18,6 @@ public void AddSwitches_registers_core_services()
Assert.Contains(services, d => d.ServiceType == typeof(SwitchPairingCoordinator));
Assert.Contains(services, d => d.ServiceType == typeof(SwitchStateRelay));
Assert.Contains(services, d => d.ServiceType == typeof(SwitchEmbedRenderer));
- Assert.Contains(services, d => d.ServiceType == typeof(ISwitchLocalizer));
+ Assert.Contains(services, d => d.ServiceType == typeof(ILocalizer));
}
}
diff --git a/tests/RustPlusBot.Features.Switches.Tests/SwitchStateRelayTests.cs b/tests/RustPlusBot.Features.Switches.Tests/SwitchStateRelayTests.cs
index 9f649df..19578db 100644
--- a/tests/RustPlusBot.Features.Switches.Tests/SwitchStateRelayTests.cs
+++ b/tests/RustPlusBot.Features.Switches.Tests/SwitchStateRelayTests.cs
@@ -7,6 +7,7 @@
using RustPlusBot.Features.Switches.Relaying;
using RustPlusBot.Features.Switches.Rendering;
using RustPlusBot.Features.Workspace.Locating;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Connections;
using RustPlusBot.Persistence.Switches;
using RustPlusBot.Persistence.Workspace;
@@ -33,7 +34,7 @@ private static Harness Create()
var poster = Substitute.For();
poster.EnsureAsync(Arg.Any(), Arg.Any(), Arg.Any(),
Arg.Any(), Arg.Any()).Returns((ulong?)900UL);
- var renderer = new SwitchEmbedRenderer(new SwitchLocalizer(SwitchLocalizationCatalog.Default));
+ var renderer = new SwitchEmbedRenderer(new ResxLocalizer());
var relay = new SwitchStateRelay(provider.GetRequiredService(), locator, poster,
renderer);
diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Localization/LocalizerTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Localization/LocalizerTests.cs
deleted file mode 100644
index dd66114..0000000
--- a/tests/RustPlusBot.Features.Workspace.Tests/Localization/LocalizerTests.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using RustPlusBot.Features.Workspace.Localization;
-
-namespace RustPlusBot.Features.Workspace.Tests.Localization;
-
-public sealed class LocalizerTests
-{
- private static Localizer NewLocalizer() => new(LocalizationCatalog.Default);
-
- [Fact]
- public void Get_ReturnsCultureSpecificValue()
- {
- var localizer = NewLocalizer();
- Assert.Equal("information", localizer.Get("channel.information.name", "en"));
- Assert.Equal("informations", localizer.Get("channel.information.name", "fr"));
- }
-
- [Fact]
- public void Get_FallsBackToEnglish_WhenCultureMissing()
- {
- var localizer = NewLocalizer();
- Assert.Equal("information", localizer.Get("channel.information.name", "de"));
- }
-
- [Fact]
- public void Get_ReturnsKey_WhenMissingEverywhere()
- {
- var localizer = NewLocalizer();
- Assert.Equal("nope.key", localizer.Get("nope.key", "en"));
- }
-
- [Fact]
- public void Get_NormalizesRegionVariants()
- {
- var localizer = NewLocalizer();
- Assert.Equal("information", localizer.Get("channel.information.name", "en-US"));
- }
-
- [Fact]
- public void Get_WithArgs_Formats()
- {
- var localizer = NewLocalizer();
- Assert.Contains("3", localizer.Get("information.servers", "en", 3), StringComparison.Ordinal);
- }
-}
diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs
index 87edc25..3b0591e 100644
--- a/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs
+++ b/tests/RustPlusBot.Features.Workspace.Tests/Messages/MapControlMessageRendererTests.cs
@@ -1,15 +1,15 @@
using Discord;
using NSubstitute;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Messages;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Map;
namespace RustPlusBot.Features.Workspace.Tests.Messages;
public sealed class MapControlMessageRendererTests
{
- private static readonly Localizer Loc = new(LocalizationCatalog.Default);
+ private static readonly ResxLocalizer Loc = new();
[Fact]
public async Task Renders_six_toggle_buttons_reflecting_settings()
diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs
index 0f3af71..e3f0de7 100644
--- a/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs
+++ b/tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs
@@ -4,9 +4,9 @@
using RustPlusBot.Domain.Connections;
using RustPlusBot.Domain.Credentials;
using RustPlusBot.Domain.Servers;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Messages;
using RustPlusBot.Features.Workspace.Registry;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Connections;
using RustPlusBot.Persistence.Servers;
using DomainConnectionState = RustPlusBot.Domain.Connections.ConnectionState;
@@ -15,7 +15,7 @@ namespace RustPlusBot.Features.Workspace.Tests.Messages;
public sealed class RendererTests
{
- private static readonly Localizer Loc = new(LocalizationCatalog.Default);
+ private static readonly ResxLocalizer Loc = new();
private static readonly MessageRenderContext Global = new(1, null, "en");
[Fact]
diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ReconcilerHarness.cs b/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ReconcilerHarness.cs
index ae76bbe..74d8128 100644
--- a/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ReconcilerHarness.cs
+++ b/tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ReconcilerHarness.cs
@@ -2,10 +2,10 @@
using NSubstitute;
using RustPlusBot.Domain.Servers;
using RustPlusBot.Features.Workspace.Gateway;
-using RustPlusBot.Features.Workspace.Localization;
using RustPlusBot.Features.Workspace.Reconciler;
using RustPlusBot.Features.Workspace.Registry;
using RustPlusBot.Features.Workspace.Tests.Fakes;
+using RustPlusBot.Localization;
using RustPlusBot.Persistence.Servers;
namespace RustPlusBot.Features.Workspace.Tests.Reconciler;
@@ -40,7 +40,7 @@ public WorkspaceReconciler Build()
var registry = new WorkspaceRegistry(_channelProviders, _messageProviders);
return new WorkspaceReconciler(
new WorkspaceBackends(registry, Gateway, Store), _renderers, Servers,
- new Localizer(LocalizationCatalog.Default), new ProvisioningLock(),
+ new ResxLocalizer(), new ProvisioningLock(),
NullLogger.Instance);
}
@@ -82,7 +82,7 @@ public ReconcilerBuilderReusing WithChannel(WorkspaceScope scope, string key, st
new WorkspaceBackends(new WorkspaceRegistry(_channelProviders, _messageProviders), source.Gateway,
source.Store),
_renderers, source.Servers,
- new Localizer(LocalizationCatalog.Default), new ProvisioningLock(),
+ new ResxLocalizer(), new ProvisioningLock(),
NullLogger.Instance);
private sealed class ListChannelProvider(IEnumerable specs) : IChannelSpecProvider
diff --git a/tests/RustPlusBot.Features.Workspace.Tests/Specs/ServerWorkspaceSpecProviderTests.cs b/tests/RustPlusBot.Features.Workspace.Tests/Specs/ServerWorkspaceSpecProviderTests.cs
index 173a22b..9e002ff 100644
--- a/tests/RustPlusBot.Features.Workspace.Tests/Specs/ServerWorkspaceSpecProviderTests.cs
+++ b/tests/RustPlusBot.Features.Workspace.Tests/Specs/ServerWorkspaceSpecProviderTests.cs
@@ -2,7 +2,7 @@
using RustPlusBot.Features.Workspace.Registry;
using RustPlusBot.Features.Workspace.Specs;
-namespace RustPlusBot.Features.Workspace.Tests;
+namespace RustPlusBot.Features.Workspace.Tests.Specs;
public sealed class ServerWorkspaceSpecProviderTests
{
diff --git a/tests/RustPlusBot.Localization.Tests/DictionaryLocalizerTests.cs b/tests/RustPlusBot.Localization.Tests/DictionaryLocalizerTests.cs
deleted file mode 100644
index 61ae923..0000000
--- a/tests/RustPlusBot.Localization.Tests/DictionaryLocalizerTests.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using RustPlusBot.Localization;
-
-namespace RustPlusBot.Localization.Tests;
-
-public sealed class DictionaryLocalizerTests
-{
- private static DictionaryLocalizer Build() => new(
- new Dictionary>(StringComparer.Ordinal)
- {
- ["en"] = new Dictionary(StringComparer.Ordinal)
- {
- ["greet"] = "Hello {0}", ["bye"] = "Bye",
- },
- ["fr"] = new Dictionary(StringComparer.Ordinal)
- {
- ["greet"] = "Bonjour {0}"
- },
- });
-
- [Fact]
- public void Get_ReturnsCultureValue() => Assert.Equal("Bonjour {0}", Build().Get("greet", "fr"));
-
- [Fact]
- public void Get_NormalizesRegion() => Assert.Equal("Bonjour {0}", Build().Get("greet", "fr-FR"));
-
- [Fact]
- public void Get_FallsBackToEnglish() => Assert.Equal("Bye", Build().Get("bye", "fr"));
-
- [Fact]
- public void Get_ReturnsKeyWhenMissing() => Assert.Equal("nope", Build().Get("nope", "en"));
-
- [Fact]
- public void Get_FormatsArgs() => Assert.Equal("Hello world", Build().Get("greet", "en", "world"));
-
- [Fact]
- public void Get_BlankCulture_UsesEnglish() => Assert.Equal("Bye", Build().Get("bye", ""));
-}
diff --git a/tests/RustPlusBot.Localization.Tests/ResxLocalizerTests.cs b/tests/RustPlusBot.Localization.Tests/ResxLocalizerTests.cs
new file mode 100644
index 0000000..d1de8f5
--- /dev/null
+++ b/tests/RustPlusBot.Localization.Tests/ResxLocalizerTests.cs
@@ -0,0 +1,63 @@
+using RustPlusBot.Localization;
+
+namespace RustPlusBot.Localization.Tests;
+
+public sealed class ResxLocalizerTests
+{
+ private static readonly ResxLocalizer Sut = new();
+
+ [Fact]
+ public void Get_returns_english_value()
+ {
+ Assert.Equal("⚡ ON", Sut.Get("switch.status.on", "en"));
+ }
+
+ [Fact]
+ public void Get_returns_french_value()
+ {
+ Assert.Equal("⚡ ALLUMÉ", Sut.Get("switch.status.on", "fr"));
+ }
+
+ [Fact]
+ public void Get_falls_back_to_english_for_unknown_culture()
+ {
+ Assert.Equal("⚡ ON", Sut.Get("switch.status.on", "de"));
+ }
+
+ [Fact]
+ public void Get_normalizes_region_specific_culture()
+ {
+ Assert.Equal("⚡ ALLUMÉ", Sut.Get("switch.status.on", "fr-FR"));
+ }
+
+ [Fact]
+ public void Get_falls_back_to_english_for_blank_culture()
+ {
+ Assert.Equal("⚡ ON", Sut.Get("switch.status.on", ""));
+ }
+
+ [Fact]
+ public void Get_returns_key_when_missing()
+ {
+ Assert.Equal("nonexistent.key", Sut.Get("nonexistent.key", "en"));
+ }
+
+ [Fact]
+ public void Get_with_args_formats_english()
+ {
+ Assert.Equal("Endpoint: 1.2.3.4:28015", Sut.Get("server.info.endpoint", "en", "1.2.3.4", 28015));
+ }
+
+ [Fact]
+ public void Get_with_args_formats_french()
+ {
+ Assert.Equal("Adresse : 1.2.3.4:28015", Sut.Get("server.info.endpoint", "fr", "1.2.3.4", 28015));
+ }
+
+ [Fact]
+ public void Get_with_args_on_missing_key_formats_the_key()
+ {
+ // Missing key returns the key itself; with no placeholders, args are ignored.
+ Assert.Equal("nonexistent.key", Sut.Get("nonexistent.key", "en", "x"));
+ }
+}
diff --git a/tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs b/tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs
new file mode 100644
index 0000000..0a3b6f6
--- /dev/null
+++ b/tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs
@@ -0,0 +1,47 @@
+using System.Collections;
+using System.Globalization;
+using System.Resources;
+using RustPlusBot.Localization;
+
+namespace RustPlusBot.Localization.Tests;
+
+public sealed class StringsResourceParityTests
+{
+ private static readonly ResourceManager Resources =
+ new("RustPlusBot.Localization.Strings", typeof(ResxLocalizer).Assembly);
+
+ private static HashSet Keys(CultureInfo culture)
+ {
+ // Do not dispose: ResourceManager caches and reuses the returned set instance,
+ // so disposing it here would break the next culture's lookup.
+ var set = Resources.GetResourceSet(culture, createIfNotExists: true, tryParents: false)!;
+ return set.Cast()
+ .Select(e => (string)e.Key)
+ .ToHashSet(StringComparer.Ordinal);
+ }
+
+ private static HashSet EnglishKeys() =>
+ // English lives in the neutral set (NeutralResourcesLanguage("en")), keyed by
+ // the invariant culture — there is no "en" satellite to read.
+ Keys(CultureInfo.InvariantCulture);
+
+ private static HashSet FrenchKeys() => Keys(CultureInfo.GetCultureInfo("fr"));
+
+ [Fact]
+ public void French_covers_every_english_key()
+ {
+ Assert.Empty(EnglishKeys().Except(FrenchKeys()));
+ }
+
+ [Fact]
+ public void English_covers_every_french_key()
+ {
+ Assert.Empty(FrenchKeys().Except(EnglishKeys()));
+ }
+
+ [Fact]
+ public void Catalog_has_expected_key_count()
+ {
+ Assert.Equal(193, EnglishKeys().Count);
+ }
+}