Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/AdminPanelLocalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Admin panel localization

Select **简体中文** in the admin panel language selector to use the Simplified
Chinese (`zh-CN`) resources. The selected culture is preserved by the existing
culture cookie. English remains the neutral resource language and fallback.

## Coverage

The translations cover administration pages, shared form controls, model labels
and descriptions, enum captions, configuration summaries, plugin metadata,
plugin configuration fields, and configuration update descriptions.

Regional culture names are preserved during culture selection and request
localization. Culture discovery also checks deployed satellite assemblies, so
`zh-CN` is available on platforms where ICU enumerates a different Chinese
culture name.

Plugin extension point names and authorization roles are translated for display.
Their stored identifiers are unchanged. Protocol identifiers, command words,
external plugin text without a translation retain their original text. Model
`ToString()` summaries are culture-dependent and may also appear localized in
logs.

Names stored in game configuration data are separate from UI resource files.
Adding these resources does not translate existing database records or alter
configuration initialization, gameplay values, or database schemas.

## Maintaining translations

Add neutral text to the corresponding `Properties/*.resx` file. Chinese
translations are optional: missing keys fall back to English and do not block
other contributions. When adding a translation to `*.zh-CN.resx`, use the same
key and keep formatting placeholders unchanged. Model captions use the naming
conventions in `ModelResourceProvider`;
plugin `DisplayAttribute` metadata must reference public resource properties.
Use `{Type}_{Property}_Name` / `_Description` for property metadata and
`{Type}_Name` / `_Description` for type metadata, rather than numbered keys.

Keep contribution descriptions, source comments, and documentation in English.
Do not translate command syntax, packet identifiers, or authorization values.

Run the admin panel tests with:

```sh
dotnet test tests/MUnique.OpenMU.Web.Tests/MUnique.OpenMU.Web.Tests.csproj \
-c Release -p:ci=true
```

Localization tests cover culture selection, enum round trips, model captions and
summaries, plugin resource metadata, English fallback and translated
placeholders,
and paging with the shared grid state.
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/AreaSkillSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,6 @@ public partial class AreaSkillSettings
/// <inheritdoc />
public override string ToString()
{
return "Area Skill Settings";
return typeof(AreaSkillSettings).GetTypeCaption();
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/BattleZoneDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ public partial class BattleZoneDefinition
/// <inheritdoc />
public override string ToString()
{
return $"Battle Zone ({this.Type})";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.BattleZoneSummary, ModelResourceProvider.GetEnumCaption(this.Type.GetType(), this.Type));
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/CastleSiegeConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,6 @@ public partial class CastleSiegeConfiguration
/// <inheritdoc />
public override string ToString()
{
return "Castle Siege Configuration";
return typeof(CastleSiegeConfiguration).GetTypeCaption();
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/CastleSiegeNpcDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ public partial class CastleSiegeNpcDefinition
/// <inheritdoc />
public override string ToString()
{
return $"{this.MonsterDefinition} #{this.InstanceId} at ({this.SpawnX},{this.SpawnY})";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.CastleNpcSummary, this.MonsterDefinition, this.InstanceId, this.SpawnX, this.SpawnY);
}
}
7 changes: 6 additions & 1 deletion src/DataModel/Configuration/CastleSiegeStateScheduleEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public partial class CastleSiegeStateScheduleEntry
/// <inheritdoc />
public override string ToString()
{
return $"{this.State} on {this.DayOfWeek} at {this.Hour:D2}:{this.Minute:D2}";
return string.Format(
Properties.Resources.CastleSiegeScheduleSummary,
this.State.GetEnumCaption(),
CultureHelper.GetDayName(this.DayOfWeek),
this.Hour,
this.Minute);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ public partial class CastleSiegeUpgradeDefinition
/// <inheritdoc />
public override string ToString()
{
return $"Level {this.Level}: Value={this.Value}, Jewels={this.RequiredJewelOfGuardianCount}, Zen={this.RequiredZen}";
return string.Format(Properties.Resources.CastleSiegeUpgradeSummary, this.Level, this.Value, this.RequiredJewelOfGuardianCount, this.RequiredZen);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/CastleSiegeZoneDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ public partial class CastleSiegeZoneDefinition
/// <inheritdoc />
public override string ToString()
{
return $"{this.X1} / {this.Y1} to {this.X2} / {this.Y2}";
return string.Format(Properties.Resources.CastleSiegeZoneSummary, this.X1, this.Y1, this.X2, this.Y2);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/ChatServerDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ public partial class ChatServerDefinition
/// <inheritdoc/>
public override string ToString()
{
return $"[ChatServerDefinition ServerID={this.ServerId}, Description={this.Description}]";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.ChatServerSummary, this.ServerId, this.Description);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/DuelConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ public partial class DuelConfiguration
/// <inheritdoc />
public override string ToString()
{
return $"Duel Configuration {this.DuelAreas.Count} Arenas, Exit: {this.Exit}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.DuelConfigSummary, this.DuelAreas.Count, this.Exit);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/EnterGate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ public partial class EnterGate : Gate
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()} ({this.Number}) (Level {this.LevelRequirement}) to {this.TargetGate}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.EnterGateSummary, base.ToString(), this.Number, this.LevelRequirement, this.TargetGate);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/GameConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,6 @@ public partial class GameConfiguration
/// <inheritdoc />
public override string ToString()
{
return "Default Game Configuration";
return typeof(GameConfiguration).GetTypeCaption();
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/GameServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public partial class GameServerConfiguration
/// <inheritdoc />
public override string ToString()
{
return $"Default ({this.MaximumPlayers} players)"; // TODO Add Description field
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.GameServerConfigSummary, this.MaximumPlayers); // TODO Add Description field
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public override string ToString()
string itemName;
if (!this.PossibleItems.Any())
{
itemName = "Random Item";
itemName = MUnique.OpenMU.DataModel.Properties.Resources.RandomItemSummary;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ public partial class CombinationBonusRequirement
/// <inheritdoc />
public override string ToString()
{
return $"{this.OptionType}: SubOption Type {this.SubOptionType}, Min. Count {this.MinimumCount}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.OptionRequirementSummary, this.OptionType, this.SubOptionType, this.MinimumCount);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/Items/ItemOptionOfLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ public partial class ItemOptionOfLevel
/// <inheritdoc />
public override string ToString()
{
return $"Level {this.Level}: {this.PowerUpDefinition}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.LevelValueSummary, this.Level, this.PowerUpDefinition);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/Items/LevelBonus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ public IElement GetAdditionalValueElement(AggregateType aggregateType)
/// <inheritdoc />
public override string ToString()
{
return $"Level: {this.Level}: {this.AdditionalValue}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.LevelValueSummary, this.Level, this.AdditionalValue);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/LevelDependentDamage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ public partial class LevelDependentDamage
/// <inheritdoc />
public override string ToString()
{
return $"Level {this.Level}: {this.Damage}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.LevelValueSummary, this.Level, this.Damage);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/MasterSkillDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,6 @@ public partial class MasterSkillDefinition
/// <inheritdoc />
public override string ToString()
{
return "Master Skill Definition";
return typeof(MasterSkillDefinition).GetTypeCaption();
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/MiniGameSpawnWave.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ public partial class MiniGameSpawnWave
/// <inheritdoc />
public override string ToString()
{
return $"Wave {this.WaveNumber}: {this.Description}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.WaveSummary, this.WaveNumber, this.Description);
}
}
10 changes: 9 additions & 1 deletion src/DataModel/Configuration/MiniGameTerrainChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ public partial class MiniGameTerrainChange
/// <inheritdoc />
public override string ToString()
{
return $"{(this.SetTerrainAttribute ? "Set" : "Remove")} Attribute '{this.TerrainAttribute}' from ({this.StartX}, {this.StartY}) to ({this.EndX}, {this.EndY}) {(this.IsClientUpdateRequired ? "with" : "without")} client update";
return string.Format(
MUnique.OpenMU.DataModel.Properties.Resources.TerrainChangeSummary,
this.SetTerrainAttribute ? MUnique.OpenMU.DataModel.Properties.Resources.TerrainSet : MUnique.OpenMU.DataModel.Properties.Resources.TerrainRemove,
ModelResourceProvider.GetEnumCaption(this.TerrainAttribute.GetType(), this.TerrainAttribute),
this.StartX,
this.StartY,
this.EndX,
this.EndY,
this.IsClientUpdateRequired ? MUnique.OpenMU.DataModel.Properties.Resources.TerrainWithClientUpdate : MUnique.OpenMU.DataModel.Properties.Resources.TerrainWithoutClientUpdate);
}
}
6 changes: 3 additions & 3 deletions src/DataModel/Configuration/MonsterSpawnArea.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,12 @@ public override string ToString()
{
var isPoint = this.IsPoint();
var result = isPoint
? $"{this.MonsterDefinition?.Designation} - Qty: {this.Quantity} @ {this.X1}/{this.Y1}"
: $"{this.MonsterDefinition?.Designation} - Qty: {this.Quantity} @ {this.X1}/{this.Y1} to {this.X2}/{this.Y2}";
? string.Format(MUnique.OpenMU.DataModel.Properties.Resources.SpawnPointSummary, this.MonsterDefinition?.Designation, this.Quantity, this.X1, this.Y1)
: string.Format(MUnique.OpenMU.DataModel.Properties.Resources.SpawnAreaSummary, this.MonsterDefinition?.Designation, this.Quantity, this.X1, this.Y1, this.X2, this.Y2);

if (this.SpawnTrigger == SpawnTrigger.AutomaticDuringWave || this.SpawnTrigger == SpawnTrigger.OnceAtWaveStart)
{
result += $" - Wave: {this.WaveNumber}";
result += string.Format(MUnique.OpenMU.DataModel.Properties.Resources.SpawnWaveSummary, this.WaveNumber);
}

return result;
Expand Down
8 changes: 4 additions & 4 deletions src/DataModel/Configuration/Quests/QuestReward.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,19 @@ public override string ToString()

if (this.RewardType == QuestRewardType.Skill)
{
return $"Skill: {this.SkillReward?.Name}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.SkillRewardSummary, this.SkillReward?.Name);
}

if (this.RewardType == QuestRewardType.Attribute)
{
return $"Attribute: {this.Value} x {this.AttributeReward}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.AttributeRewardSummary, this.Value, this.AttributeReward);
}

if (this.RewardType is QuestRewardType.Experience or QuestRewardType.Money or QuestRewardType.LevelUpPoints or QuestRewardType.GensAttribution)
{
return $"{this.Value} x {this.RewardType}";
return $"{this.Value} x {ModelResourceProvider.GetEnumCaption(this.RewardType.GetType(), this.RewardType)}";
}

return $"{this.RewardType}";
return $"{ModelResourceProvider.GetEnumCaption(this.RewardType.GetType(), this.RewardType)}";
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/Rectangle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public override string ToString()
return $"{this.X1} / {this.Y1}";
}

return $"{this.X1} / {this.Y1} to {this.X2} / {this.Y2}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.RectangleSummary, this.X1, this.Y1, this.X2, this.Y2);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/ServerEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ protected ServerEndpoint()
/// </returns>
public override string ToString()
{
return $"Client: {this.Client?.Description}; Port: {this.NetworkPort}";
return string.Format(MUnique.OpenMU.DataModel.Properties.Resources.ServerEndpointSummary, this.Client?.Description, this.NetworkPort);
}
}
2 changes: 1 addition & 1 deletion src/DataModel/Configuration/SystemConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,6 @@ public partial class SystemConfiguration
/// <inheritdoc />
public override string ToString()
{
return "System Configuration";
return Resources.SystemConfiguration_Name;
}
}
27 changes: 25 additions & 2 deletions src/DataModel/CultureHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace MUnique.OpenMU.DataModel;

using System.Globalization;
using System.IO;
using System.Resources;
using Nito.Disposables;
using Nito.Disposables.Internals;
Expand Down Expand Up @@ -35,16 +36,38 @@ public static IDisposable SetTemporaryCulture(CultureInfo cultureInfo)
});
}

/// <summary>
/// Gets a weekday caption, preferring deployed resources over platform culture data.
/// </summary>
/// <param name="day">The weekday.</param>
/// <returns>The localized weekday name.</returns>
public static string GetDayName(DayOfWeek day)
{
var culture = CultureInfo.CurrentUICulture;
var resources = Properties.Resources.ResourceManager.GetResourceSet(culture, true, false);
return resources?.GetString($"DayOfWeek_{day}") ?? culture.DateTimeFormat.GetDayName(day);
}

/// <summary>
/// Gets the available cultures for a specific resource.
/// </summary>
/// <typeparam name="TResources">The type of the resources.</typeparam>
/// <returns>An enumeration of <see cref="CultureInfo.TwoLetterISOLanguageName"/> of available cultures of the given resource type.</returns>
/// <returns>The available cultures of the given resource type.</returns>
public static IEnumerable<CultureInfo> GetAvailableCultures<TResources>()
{
var resourceManager = new ResourceManager(typeof(TResources));

var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
// ICU can enumerate zh-Hans-CN instead of zh-CN. Include the exact culture names
// of deployed satellite assemblies so regional resources are found on every platform.
var assembly = typeof(TResources).Assembly;
var directory = Path.GetDirectoryName(assembly.Location) ?? AppContext.BaseDirectory;
var satelliteName = $"{assembly.GetName().Name}.resources.dll";
var deployedCultures = Directory.EnumerateDirectories(directory)
.Where(path => File.Exists(Path.Combine(path, satelliteName)))
.Select(path => CultureInfo.GetCultureInfo(Path.GetFileName(path)));
var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures)
.Concat(deployedCultures)
.Distinct();
var result = cultures
.Except([CultureInfo.InvariantCulture])
.Where(culture => culture is { IsNeutralCulture: true, TwoLetterISOLanguageName: "en" }
Expand Down
2 changes: 1 addition & 1 deletion src/DataModel/Entities/ItemStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public partial class ItemStorage
/// <inheritdoc />
public override string ToString()
{
return $"{this.Items?.Count ?? 0} Items, {this.Money} Money";
return string.Format(Properties.Resources.ItemStorageSummary, this.Items?.Count ?? 0, this.Money);
}
}
Loading
Loading