fix broken unit tests

This commit is contained in:
Jesse Plamondon-Willard 2022-04-16 18:29:52 -04:00
parent 20224d293d
commit 7dec519234
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
7 changed files with 68 additions and 50 deletions

View File

@ -87,7 +87,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
[TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")] [TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")]
[TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type [TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type
[TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")] [TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")]
[TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetInt", "int")] // ↓ explicit conversion to invalid type [TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetFieldBase", "int")] // ↓ explicit conversion to invalid type
[TestCase("Item item = new Item(); int x = item.netRefField as object;", 32, "item.netRefField", "NetRef", "object")] [TestCase("Item item = new Item(); int x = item.netRefField as object;", 32, "item.netRefField", "NetRef", "object")]
public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType) public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType)
{ {

View File

@ -78,9 +78,9 @@ namespace SMAPI.Tests.Core
[TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
[TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)]
// whitespace-sensitive // whitespace-insensitive
[TestCase("Data/Achievements", " Data/Achievements ", ExpectedResult = false)] [TestCase("Data/Achievements", " Data/Achievements ", ExpectedResult = true)]
[TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
// other is null or whitespace // other is null or whitespace
[TestCase("Data/Achievements", null, ExpectedResult = false)] [TestCase("Data/Achievements", null, ExpectedResult = false)]
@ -109,7 +109,7 @@ namespace SMAPI.Tests.Core
[TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)] [TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)]
[TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] [TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
[TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)]
[TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
[TestCase("Data/Achievements", " ", ExpectedResult = false)] [TestCase("Data/Achievements", " ", ExpectedResult = false)]
// with locale codes // with locale codes
@ -141,13 +141,13 @@ namespace SMAPI.Tests.Core
[TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)] [TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
[TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)] [TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)]
// leading-whitespace-sensitive // whitespace-insensitive
[TestCase("Data/Achievements", " Data/Achievements", ExpectedResult = false)] [TestCase("Data/Achievements", " Data/Achievements", ExpectedResult = true)]
[TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = false)] [TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
[TestCase("Data/Achievements", " ", ExpectedResult = true)]
// invalid prefixes // invalid prefixes
[TestCase("Data/Achievements", null, ExpectedResult = false)] [TestCase("Data/Achievements", null, ExpectedResult = false)]
[TestCase("Data/Achievements", " ", ExpectedResult = false)]
// with locale codes // with locale codes
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)] [TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)]

View File

@ -38,6 +38,9 @@ namespace SMAPI.Tests.Core
// assert // assert
Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead.");
// cleanup
Directory.Delete(rootFolder, recursive: true);
} }
[Test(Description = "Assert that the resolver correctly returns a failed metadata if there's an empty mod folder.")] [Test(Description = "Assert that the resolver correctly returns a failed metadata if there's an empty mod folder.")]
@ -56,6 +59,9 @@ namespace SMAPI.Tests.Core
Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead."); Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead.");
Assert.AreEqual(ModMetadataStatus.Failed, mod!.Status, "The mod metadata was not marked failed."); Assert.AreEqual(ModMetadataStatus.Failed, mod!.Status, "The mod metadata was not marked failed.");
Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set.");
// cleanup
Directory.Delete(rootFolder, recursive: true);
} }
[Test(Description = "Assert that the resolver correctly reads manifest data from a randomized file.")] [Test(Description = "Assert that the resolver correctly reads manifest data from a randomized file.")]
@ -115,6 +121,9 @@ namespace SMAPI.Tests.Core
Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null."); Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null.");
Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value."); Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value.");
Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match."); Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match.");
// cleanup
Directory.Delete(rootFolder, recursive: true);
} }
/**** /****
@ -123,7 +132,7 @@ namespace SMAPI.Tests.Core
[Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")]
public void ValidateManifests_NoMods_DoesNothing() public void ValidateManifests_NoMods_DoesNothing()
{ {
new ModResolver().ValidateManifests(Array.Empty<ModMetadata>(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); new ModResolver().ValidateManifests(Array.Empty<ModMetadata>(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
} }
[Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")]
@ -134,7 +143,7 @@ namespace SMAPI.Tests.Core
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
// act // act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
// assert // assert
mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status.");
@ -145,13 +154,13 @@ namespace SMAPI.Tests.Core
{ {
// arrange // arrange
Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true); Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields(this.GetModDataRecord()) mock.Setup(p => p.DataRecord).Returns(() => new ModDataRecordVersionedFields(this.GetModDataRecord())
{ {
Status = ModStatus.AssumeBroken Status = ModStatus.AssumeBroken
}); });
// act // act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
// assert // assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata."); mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@ -163,10 +172,9 @@ namespace SMAPI.Tests.Core
// arrange // arrange
Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true); Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1")); mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1"));
this.SetupMetadataForValidation(mock);
// act // act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
// assert // assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata."); mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@ -176,14 +184,18 @@ namespace SMAPI.Tests.Core
public void ValidateManifests_MissingEntryDLL_Fails() public void ValidateManifests_MissingEntryDLL_Fails()
{ {
// arrange // arrange
Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true); string directoryPath = this.GetTempFolderPath();
this.SetupMetadataForValidation(mock); Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true, directoryPath: directoryPath);
Directory.CreateDirectory(directoryPath);
// act // act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null);
// assert // assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata."); mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
// cleanup
Directory.Delete(directoryPath);
} }
[Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")]
@ -192,16 +204,13 @@ namespace SMAPI.Tests.Core
// arrange // arrange
Mock<IModMetadata> modA = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true); Mock<IModMetadata> modA = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true); Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true);
Mock<IModMetadata> modC = this.GetMetadata("Mod C", Array.Empty<string>(), allowStatusChange: false);
foreach (Mock<IModMetadata> mod in new[] { modA, modB, modC })
this.SetupMetadataForValidation(mod);
// act // act
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null); new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
// assert // assert
modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the first mod with a unique ID."); modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the first mod with a unique ID.");
modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the second mod with a unique ID."); modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the second mod with a unique ID.");
} }
[Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")]
@ -227,6 +236,9 @@ namespace SMAPI.Tests.Core
// assert // assert
// if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status.
// cleanup
Directory.Delete(modFolder, recursive: true);
} }
/**** /****
@ -514,14 +526,20 @@ namespace SMAPI.Tests.Core
/// <summary>Get a randomized basic manifest.</summary> /// <summary>Get a randomized basic manifest.</summary>
/// <param name="manifest">The mod manifest.</param> /// <param name="manifest">The mod manifest.</param>
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param> /// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false) /// <param name="directoryPath">The directory path the mod metadata should be pointed at, or <c>null</c> to generate a fake path.</param>
private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false, string? directoryPath = null)
{ {
directoryPath ??= this.GetTempFolderPath();
Mock<IModMetadata> mod = new(MockBehavior.Strict); Mock<IModMetadata> mod = new(MockBehavior.Strict);
mod.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields()); mod.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields());
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID);
mod.Setup(p => p.DirectoryPath).Returns(directoryPath);
mod.Setup(p => p.Manifest).Returns(manifest); mod.Setup(p => p.Manifest).Returns(manifest);
mod.Setup(p => p.HasID(It.IsAny<string>())).Returns((string id) => manifest.UniqueID == id); mod.Setup(p => p.HasID(It.IsAny<string>())).Returns((string id) => manifest.UniqueID == id);
mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>());
mod.Setup(p => p.GetRelativePathWithRoot()).Returns(directoryPath);
if (allowStatusChange) if (allowStatusChange)
{ {
mod mod
@ -532,18 +550,6 @@ namespace SMAPI.Tests.Core
return mod; return mod;
} }
/// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary>
/// <param name="mod">The mock mod metadata.</param>
/// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param>
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields? modRecord = null)
{
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mod.Setup(p => p.Manifest).Returns(this.GetManifest());
mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath());
mod.Setup(p => p.DataRecord).Returns(modRecord ?? this.GetModDataRecordVersionedFields());
mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>());
}
/// <summary>Generate a default mod data record.</summary> /// <summary>Generate a default mod data record.</summary>
private ModDataRecord GetModDataRecord() private ModDataRecord GetModDataRecord()
{ {

View File

@ -55,7 +55,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{ {
// get list // get list
List<string> values = !string.IsNullOrWhiteSpace(rawField) List<string> values = !string.IsNullOrWhiteSpace(rawField)
? new List<string>(rawField.Split(',')) ? new List<string>(
from field in rawField.Split(',')
let value = field.Trim()
where value.Length > 0
select value
)
: new List<string>(); : new List<string>();
// apply changes // apply changes

View File

@ -119,25 +119,27 @@ namespace StardewModdingAPI.Framework.Content
if (prefix is null) if (prefix is null)
return false; return false;
string rawTrimmed = prefix.Trim();
// asset keys can't have a leading slash, but NormalizeAssetName will trim them // asset keys can't have a leading slash, but NormalizeAssetName will trim them
{ if (rawTrimmed.StartsWith('/') || rawTrimmed.StartsWith('\\'))
string trimmed = prefix.TrimStart(); return false;
if (trimmed.StartsWith('/') || trimmed.StartsWith('\\'))
return false;
}
// normalize prefix // normalize prefix
{ {
string normalized = PathUtilities.NormalizeAssetName(prefix); string normalized = PathUtilities.NormalizeAssetName(prefix);
string trimmed = prefix.TrimEnd(); // keep trailing slash
if (trimmed.EndsWith('/') || trimmed.EndsWith('\\')) if (rawTrimmed.EndsWith('/') || rawTrimmed.EndsWith('\\'))
normalized += PathUtilities.PreferredAssetSeparator; normalized += PathUtilities.PreferredAssetSeparator;
prefix = normalized; prefix = normalized;
} }
// compare // compare
if (prefix.Length == 0)
return true;
return return
this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) this.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
&& ( && (

View File

@ -56,9 +56,10 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="mods">The mod manifests to validate.</param> /// <param name="mods">The mod manifests to validate.</param>
/// <param name="apiVersion">The current SMAPI version.</param> /// <param name="apiVersion">The current SMAPI version.</param>
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param> /// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
/// <param name="validateFilesExist">Whether to validate that files referenced in the manifest (like <see cref="IManifest.EntryDll"/>) exist on disk. This can be disabled to only validate the manifest itself.</param>
[SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "Manifest values may be null before they're validated.")] [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "Manifest values may be null before they're validated.")]
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Manifest values may be null before they're validated.")] [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Manifest values may be null before they're validated.")]
public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string?> getUpdateUrl) public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string?> getUpdateUrl, bool validateFilesExist = true)
{ {
mods = mods.ToArray(); mods = mods.ToArray();
@ -141,11 +142,14 @@ namespace StardewModdingAPI.Framework.ModLoading
} }
// file doesn't exist // file doesn't exist
string fileName = CaseInsensitivePathLookup.GetCachedFor(mod.DirectoryPath).GetFilePath(mod.Manifest.EntryDll!); if (validateFilesExist)
if (!File.Exists(Path.Combine(mod.DirectoryPath, fileName)))
{ {
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); string fileName = CaseInsensitivePathLookup.GetCachedFor(mod.DirectoryPath).GetFilePath(mod.Manifest.EntryDll!);
continue; if (!File.Exists(Path.Combine(mod.DirectoryPath, fileName)))
{
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
continue;
}
} }
} }

View File

@ -51,7 +51,8 @@ namespace StardewModdingAPI.Framework
foreach (string key in this.GetAllKeysRaw()) foreach (string key in this.GetAllKeysRaw())
{ {
string? text = this.GetRaw(key, locale, withFallback: true); string? text = this.GetRaw(key, locale, withFallback: true);
this.ForLocale.Add(key, new Translation(this.Locale, key, text)); if (text != null)
this.ForLocale.Add(key, new Translation(this.Locale, key, text));
} }
} }