Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Localization support for any prototypes #4335

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
14 changes: 14 additions & 0 deletions Robust.Shared/Localization/ILocalizationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using JetBrains.Annotations;
using Linguini.Bundle;
using Linguini.Syntax.Ast;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;

namespace Robust.Shared.Localization
Expand Down Expand Up @@ -122,6 +126,16 @@ public interface ILocalizationManager
/// Gets localization data for an entity prototype.
/// </summary>
EntityLocData GetEntityData(string prototypeId);

/// <summary>
/// Gets localization data for a prototype.
/// </summary>
PrototypeLocData GetPrototypeData(Type kind, string prototypeId);

/// <summary>
/// Gets localization data for a prototype.
/// </summary>
PrototypeLocData GetPrototypeData<T>(string prototypeId) where T : class, IPrototype, ILocalizedPrototype;
}

internal interface ILocalizationManagerInternal : ILocalizationManager
Expand Down
123 changes: 123 additions & 0 deletions Robust.Shared/Localization/LocalizationManager.Prototypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Linguini.Bundle.Errors;
using Robust.Shared.Prototypes;

namespace Robust.Shared.Localization;

internal sealed partial class LocalizationManager
{
// Concurrent dict so that multiple threads "reading" .Name won't cause a concurrent write issue
// when the cache gets populated.
private readonly ConcurrentDictionary<string, PrototypeLocData> _prototypeCache = new();
Copy link
Member

@ElectroJr ElectroJr Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be per-prototype
NVM see my earlier comment, just remove it altogether.


private void FlushPrototypeCache()
{
_logSawmill.Debug("Flushing entity localization cache.");
_entityCache.Clear();
}

// Flush caches conservatively on prototype/localization changes.
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)

Check failure on line 22 in Robust.Shared/Localization/LocalizationManager.Prototypes.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Type 'LocalizationManager' already defines a member called 'OnPrototypesReloaded' with the same parameter types

Check failure on line 22 in Robust.Shared/Localization/LocalizationManager.Prototypes.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Type 'LocalizationManager' already defines a member called 'OnPrototypesReloaded' with the same parameter types
{
if (args.ByType.ContainsKey(typeof(EntityPrototype)))
FlushEntityCache();

FlushPrototypeCache();
Comment on lines +24 to +27
Copy link
Member

@ElectroJr ElectroJr Oct 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should enumerate over kinds and only flush the cache for prototypes that actually changed.
Also FlushPrototypeCache() is broken, its just a copy paste of FlushEntityCache()

NVM see my earlier comment, just remove it altogether.

}

private PrototypeLocData CalcInheritingPrototypeLoc<T>(string prototypeId) where T : class, IPrototype, IInheritingPrototype, ILocalizedPrototype
{
return CalcInheritingPrototypeLoc(typeof(T), prototypeId);
}

private PrototypeLocData CalcInheritingPrototypeLoc(Type kind, string prototypeId)
{
string? name = null;
string? desc = null;

foreach (var prototype in _prototype.EnumerateParents(kind, prototypeId, true))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't recursively iterate up through a prototypes parents, this is just the direct parent list

{
var lproto = (ILocalizedPrototype)prototype;
var locId = $"{lproto.CustomLocalizationPrefix}-{prototypeId}";

if (TryGetMessage(locId, out var bundle, out var msg))
{
// Localization override exists.
var msgAttrs = msg.Attributes;

if (name == null && msg.Value != null)
{
// Only set name if the value isn't empty.
// So that you can override *only* a desc.
name = bundle.FormatPattern(msg.Value, null, out var fmtErr);
WriteWarningForErrs(fmtErr, locId);
}

if (msgAttrs.Count != 0)
{
var allErrors = new List<FluentError>();
if (desc == null
&& !bundle.TryGetMsg(locId, "desc", null, out var err1, out desc))
{
desc = null;
allErrors.AddRange(err1);
}

WriteWarningForErrs(allErrors, locId);
}
}

name ??= lproto.SetName;
desc ??= lproto.SetDesc;
}

return new PrototypeLocData(
name ?? "",
desc ?? "");
}

private PrototypeLocData CalcPrototypeLoc<T>(string prototypeId) where T : class, IPrototype, ILocalizedPrototype
{
return CalcPrototypeLoc(typeof(T), prototypeId);
}

private PrototypeLocData CalcPrototypeLoc(Type kind, string prototypeId)
{
string? name = null;
string? desc = null;

// Return empty strings if no prototype found
if (!_prototype.TryIndex(kind, prototypeId, out var prototype))
return new PrototypeLocData("", "");

var lproto = (ILocalizedPrototype)prototype;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try cast

var locId = $"{lproto.CustomLocalizationPrefix}-{prototypeId}";

if (!TryGetMessage(locId, out var bundle, out var msg)
|| msg.Value == null)
return new PrototypeLocData(lproto.SetName ?? "", lproto.SetDesc ?? "");

name = bundle.FormatPattern(msg.Value, null, out var fmtErr);

if (!bundle.TryGetMsg(locId, "desc", null, out var err1, out desc))
{
return new PrototypeLocData(name ?? "", lproto.SetDesc ?? "");
}

return new PrototypeLocData(name ?? "", desc);
}

public PrototypeLocData GetPrototypeData(Type kind, string prototypeId)
{
if (kind.IsAssignableTo(typeof(IInheritingPrototype)))
return _prototypeCache.GetOrAdd(prototypeId, (id, t) => t.CalcInheritingPrototypeLoc(kind, id), this);
return _prototypeCache.GetOrAdd(prototypeId, (id, t) => t.CalcPrototypeLoc(kind, id), this);
}

public PrototypeLocData GetPrototypeData<T>(string prototypeId) where T: class, IPrototype, ILocalizedPrototype
{
return GetPrototypeData(typeof(T), prototypeId);
}
}
10 changes: 10 additions & 0 deletions Robust.Shared/Localization/PrototypeLocData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Robust.Shared.Localization;

/// <summary>
/// Contains based localized prototype data.
/// </summary>
/// <param name="Name">The localized name of the prototype.</param>
/// <param name="Desc">The localized description of the prototype.</param>
public record PrototypeLocData(
string Name,
string Desc);
15 changes: 15 additions & 0 deletions Robust.Shared/Prototypes/IPrototype.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.Localization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
Expand Down Expand Up @@ -32,6 +33,20 @@ public interface IInheritingPrototype
bool Abstract { get; }
}

public interface ILocalizedPrototype
{

[DataField("name")] public string? SetName { get; }

[DataField("description")] public string? SetDesc { get; }

[ViewVariables] public string Name { get; }

[ViewVariables] public string Description { get; }
Comment on lines +39 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but I don't think the data-field attributes do anything on interfaces?
I'm also not sure about whether VV works on interfaces.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, these should have documentation comments


public string? CustomLocalizationPrefix { get; }
}

public sealed class IdDataFieldAttribute : DataFieldAttribute
{
public const string Name = "id";
Expand Down
Loading