It’s one of those things you need to when writing multilingual applications. Create resource file (resx) for each language you want to support. And then you add an item and forget to add it to one of language files and then, oops empty label.

We don’t want that.

And we write tests, so why not write a test for that.

And on Stackoverflow the user TiltonJH was so kind to provide me with the answer.

I translated it to VB.Net code and changed a small thing (the resourcemanager didn’t find the resourcesets but the resourcemaager from the type did, so I pass that in.

vb.net
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
Imports System.Globalization
Imports System.Reflection
Imports System.Resources
Imports System.Text
Imports Nancy.Testing
 
Namespace Resources
 
    Public Class ResourceTester
 
        Public Shared Sub TestResxForInconsistencies(type As Type, resourceManager As ResourceManager)
            If type Is Nothing Then Throw New ArgumentNullException(NameOf(type))
            Dim cultureResourceDictionaries = GetResxDictionaries(type, resourceManager)
            Dim emptyEntries = GetEmpty(cultureResourceDictionaries)
            Dim neutralLanguage = ExtractNeutralLanguage(cultureResourceDictionaries, type)
            Dim missingEntries = GetMissing(cultureResourceDictionaries, neutralLanguage)
            Dim dispensableEntries = GetDispensable(cultureResourceDictionaries, neutralLanguage)
            If (emptyEntries.Count > 0 OrElse missingEntries.Count > 0 OrElse dispensableEntries.Count > 0) Then
                Dim message = New StringBuilder()
                message.AppendFormat("Found resx errors in ""{0}"":", type)
                message.AppendLine()
                message.AppendLine()
                Append(message, emptyEntries, " Empty Entries ", "Entries which do not have a value.")
                Append(message, missingEntries, " Missing Entries ", "Entries which are specified in the neutral language but are missing in the specified language.")
                Append(message, dispensableEntries, " Dispensable Entries ", "Entries which are not specified in the neutral language but are present in the specified language and should be removed.")
                Throw New Nunit.Framework.AssertionException(message.ToString())
            End If
        End Sub
 
        Private Shared Sub Append(message As StringBuilder, entries As Dictionary(Of String, List(Of String)), headline As String, description As String)
            If entries.Count > 0 Then
                message.AppendLine(headline)
                message.AppendLine(New String("=", headline.Length))
                message.Append("(")
                message.Append(description)
                message.AppendLine(")")
                For Each pair In entries
                    Dim languageName = pair.Key
                    If String.IsNullOrEmpty(languageName) Then
                        languageName = "<neutral language>"
                    End If
                    Dim line = String.Format("  Language: {0}  ", languageName)
                    message.AppendLine(line)
                    message.AppendLine(New String("-", line.Length))
                    For Each key In pair.Value
                        message.Append("\t")
                        message.AppendLine(key)
                    Next
                Next
                message.AppendLine()
            End If
        End Sub
 
        Private Shared Function ExtractNeutralLanguage(resxs As Dictionary(Of String, Dictionary(Of String, Object)), type As Type) As Dictionary(Of String, Object)
            Dim neutralLanguage = New Dictionary(Of String, Object)
            If Not resxs.TryGetValue(String.Empty, neutralLanguage) Then Throw New AssertException(String.Format("The neutral language is not specified in ""{0}"".", type))
            resxs.Remove(String.Empty)
            Return neutralLanguage
        End Function
 
        Private Shared Function GetAvailableResxCultureInfos(assembly As Assembly) As CultureInfo()
            Dim assemblyResxCultures = New HashSet(Of CultureInfo)()
            assemblyResxCultures.Add(CultureInfo.InvariantCulture)
            Dim names = assembly.GetManifestResourceNames()
            If names IsNot Nothing AndAlso names.Length > 0 Then
                Dim allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures)
                Const resourcesEnding As String = ".resources"
                For i = 0 To names.Length - 1
                    Dim name = names(i)
                    If String.IsNullOrWhiteSpace(name) OrElse name.Length <= resourcesEnding.Length OrElse Not name.EndsWith(resourcesEnding, StringComparison.InvariantCultureIgnoreCase) Then
                        Continue For
                    End If
                    name = name.Remove(name.Length - resourcesEnding.Length, resourcesEnding.Length)
                    If (String.IsNullOrWhiteSpace(name)) Then
                        Continue For
                    End If
                    Dim resourceManager = New ResourceManager(name, assembly)
                    For j = 0 To allCultures.Length - 1
                        Dim culture = allCultures(j)
                        Try
                            If (culture.Equals(CultureInfo.InvariantCulture)) Then
                                Continue For
                            End If
                            Using resourceSet = resourceManager.GetResourceSet(culture, True, False)
                                If (resourceSet IsNot Nothing) Then
                                    assemblyResxCultures.Add(culture)
                                End If
                            End Using
                        Catch ex As CultureNotFoundException
 
                        End Try
                    Next
                Next
            End If
            Return assemblyResxCultures.ToArray()
        End Function
 
        Private Shared Function GetResxDictionaries(type As Type, resourceManager As resourceManager) As Dictionary(Of String, Dictionary(Of String, Object))
            Dim availableResxsCultureInfos = GetAvailableResxCultureInfos(type.Assembly)
            Dim resxDictionaries = New Dictionary(Of String, Dictionary(Of String, Object))()
            For i = 0 To availableResxsCultureInfos.Length - 1
                Dim cultureInfo = availableResxsCultureInfos(i)
                Try
                    Dim resourceSet = resourceManager.GetResourceSet(cultureInfo, True, True)
                    If resourceSet IsNot Nothing Then
                        Dim dict = New Dictionary(Of String, Object)()
                        For Each item In resourceSet
                            Dim key = item.Key.ToString()
                            Dim value = item.Value
                            dict.Add(key, value)
                        Next
                        resxDictionaries.Add(cultureInfo.Name, dict)
                    End If
                Catch ex As Exception
 
                End Try
               
            Next
            Return resxDictionaries
        End Function
 
        Private Shared Function GetDispensable(resxDictionaries As Dictionary(Of String, Dictionary(Of String, Object)), neutralLanguage As Dictionary(Of String, Object)) As Dictionary(Of String, List(Of String))
            Dim dispensable = New Dictionary(Of String, List(Of String))()
            For Each pair In resxDictionaries
                Dim resxs = pair.Value
                Dim list = New List(Of String)()
                For Each key In resxs.Keys
                    If Not neutralLanguage.ContainsKey(key) Then
                        list.Add(key)
                    End If
                Next
                If (list.Count > 0) Then
                    dispensable.Add(pair.Key, list)
                End If
            Next
            Return dispensable
        End Function
 
        Private Shared Function GetEmpty(resxDictionaries As Dictionary(Of String, Dictionary(Of String, Object))) As Dictionary(Of String, List(Of String))
            Dim empty = New Dictionary(Of String, List(Of String))()
            For Each pair In resxDictionaries
                Dim resxs = pair.Value
                Dim list = New List(Of String)()
                For Each entrie In resxs
                    If entrie.Value Is Nothing Then
                        list.Add(entrie.Key)
                    End If
                    Dim stringValue = entrie.Value
                    If (String.IsNullOrWhiteSpace(stringValue)) Then
                        list.Add(entrie.Key)
                    End If
                Next
                If (list.Count > 0) Then
                    empty.Add(pair.Key, list)
                End If
            Next
            Return empty
        End Function
 
        Private Shared Function GetMissing(resxDictionaries As Dictionary(Of String, Dictionary(Of String, Object)), neutralLanguage As Dictionary(Of String, Object)) As Dictionary(Of String, List(Of String))
            Dim missing = New Dictionary(Of String, List(Of String))()
            For Each pair In resxDictionaries
                Dim resxs = pair.Value
                Dim list = New List(Of String)()
                For Each key In neutralLanguage.Keys
                    If Not resxs.ContainsKey(key) Then
                        list.Add(key)
                    End If
                Next
                If list.Count > 0 Then missing.Add(pair.Key, list)
            Next
            Return missing
        End Function
 
    End Class
End Namespace
Imports System.Globalization
Imports System.Reflection
Imports System.Resources
Imports System.Text
Imports Nancy.Testing

Namespace Resources

    Public Class ResourceTester

        Public Shared Sub TestResxForInconsistencies(type As Type, resourceManager As ResourceManager)
            If type Is Nothing Then Throw New ArgumentNullException(NameOf(type))
            Dim cultureResourceDictionaries = GetResxDictionaries(type, resourceManager)
            Dim emptyEntries = GetEmpty(cultureResourceDictionaries)
            Dim neutralLanguage = ExtractNeutralLanguage(cultureResourceDictionaries, type)
            Dim missingEntries = GetMissing(cultureResourceDictionaries, neutralLanguage)
            Dim dispensableEntries = GetDispensable(cultureResourceDictionaries, neutralLanguage)
            If (emptyEntries.Count > 0 OrElse missingEntries.Count > 0 OrElse dispensableEntries.Count > 0) Then
                Dim message = New StringBuilder()
                message.AppendFormat("Found resx errors in ""{0}"":", type)
                message.AppendLine()
                message.AppendLine()
                Append(message, emptyEntries, " Empty Entries ", "Entries which do not have a value.")
                Append(message, missingEntries, " Missing Entries ", "Entries which are specified in the neutral language but are missing in the specified language.")
                Append(message, dispensableEntries, " Dispensable Entries ", "Entries which are not specified in the neutral language but are present in the specified language and should be removed.")
                Throw New Nunit.Framework.AssertionException(message.ToString())
            End If
        End Sub

        Private Shared Sub Append(message As StringBuilder, entries As Dictionary(Of String, List(Of String)), headline As String, description As String)
            If entries.Count > 0 Then
                message.AppendLine(headline)
                message.AppendLine(New String("=", headline.Length))
                message.Append("(")
                message.Append(description)
                message.AppendLine(")")
                For Each pair In entries
                    Dim languageName = pair.Key
                    If String.IsNullOrEmpty(languageName) Then
                        languageName = "<neutral language>"
                    End If
                    Dim line = String.Format("  Language: {0}  ", languageName)
                    message.AppendLine(line)
                    message.AppendLine(New String("-", line.Length))
                    For Each key In pair.Value
                        message.Append("\t")
                        message.AppendLine(key)
                    Next
                Next
                message.AppendLine()
            End If
        End Sub

        Private Shared Function ExtractNeutralLanguage(resxs As Dictionary(Of String, Dictionary(Of String, Object)), type As Type) As Dictionary(Of String, Object)
            Dim neutralLanguage = New Dictionary(Of String, Object)
            If Not resxs.TryGetValue(String.Empty, neutralLanguage) Then Throw New AssertException(String.Format("The neutral language is not specified in ""{0}"".", type))
            resxs.Remove(String.Empty)
            Return neutralLanguage
        End Function

        Private Shared Function GetAvailableResxCultureInfos(assembly As Assembly) As CultureInfo()
            Dim assemblyResxCultures = New HashSet(Of CultureInfo)()
            assemblyResxCultures.Add(CultureInfo.InvariantCulture)
            Dim names = assembly.GetManifestResourceNames()
            If names IsNot Nothing AndAlso names.Length > 0 Then
                Dim allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures)
                Const resourcesEnding As String = ".resources"
                For i = 0 To names.Length - 1
                    Dim name = names(i)
                    If String.IsNullOrWhiteSpace(name) OrElse name.Length <= resourcesEnding.Length OrElse Not name.EndsWith(resourcesEnding, StringComparison.InvariantCultureIgnoreCase) Then
                        Continue For
                    End If
                    name = name.Remove(name.Length - resourcesEnding.Length, resourcesEnding.Length)
                    If (String.IsNullOrWhiteSpace(name)) Then
                        Continue For
                    End If
                    Dim resourceManager = New ResourceManager(name, assembly)
                    For j = 0 To allCultures.Length - 1
                        Dim culture = allCultures(j)
                        Try
                            If (culture.Equals(CultureInfo.InvariantCulture)) Then
                                Continue For
                            End If
                            Using resourceSet = resourceManager.GetResourceSet(culture, True, False)
                                If (resourceSet IsNot Nothing) Then
                                    assemblyResxCultures.Add(culture)
                                End If
                            End Using
                        Catch ex As CultureNotFoundException

                        End Try
                    Next
                Next
            End If
            Return assemblyResxCultures.ToArray()
        End Function

        Private Shared Function GetResxDictionaries(type As Type, resourceManager As resourceManager) As Dictionary(Of String, Dictionary(Of String, Object))
            Dim availableResxsCultureInfos = GetAvailableResxCultureInfos(type.Assembly)
            Dim resxDictionaries = New Dictionary(Of String, Dictionary(Of String, Object))()
            For i = 0 To availableResxsCultureInfos.Length - 1
                Dim cultureInfo = availableResxsCultureInfos(i)
                Try
                    Dim resourceSet = resourceManager.GetResourceSet(cultureInfo, True, True)
                    If resourceSet IsNot Nothing Then
                        Dim dict = New Dictionary(Of String, Object)()
                        For Each item In resourceSet
                            Dim key = item.Key.ToString()
                            Dim value = item.Value
                            dict.Add(key, value)
                        Next
                        resxDictionaries.Add(cultureInfo.Name, dict)
                    End If
                Catch ex As Exception

                End Try
               
            Next
            Return resxDictionaries
        End Function

        Private Shared Function GetDispensable(resxDictionaries As Dictionary(Of String, Dictionary(Of String, Object)), neutralLanguage As Dictionary(Of String, Object)) As Dictionary(Of String, List(Of String))
            Dim dispensable = New Dictionary(Of String, List(Of String))()
            For Each pair In resxDictionaries
                Dim resxs = pair.Value
                Dim list = New List(Of String)()
                For Each key In resxs.Keys
                    If Not neutralLanguage.ContainsKey(key) Then
                        list.Add(key)
                    End If
                Next
                If (list.Count > 0) Then
                    dispensable.Add(pair.Key, list)
                End If
            Next
            Return dispensable
        End Function

        Private Shared Function GetEmpty(resxDictionaries As Dictionary(Of String, Dictionary(Of String, Object))) As Dictionary(Of String, List(Of String))
            Dim empty = New Dictionary(Of String, List(Of String))()
            For Each pair In resxDictionaries
                Dim resxs = pair.Value
                Dim list = New List(Of String)()
                For Each entrie In resxs
                    If entrie.Value Is Nothing Then
                        list.Add(entrie.Key)
                    End If
                    Dim stringValue = entrie.Value
                    If (String.IsNullOrWhiteSpace(stringValue)) Then
                        list.Add(entrie.Key)
                    End If
                Next
                If (list.Count > 0) Then
                    empty.Add(pair.Key, list)
                End If
            Next
            Return empty
        End Function

        Private Shared Function GetMissing(resxDictionaries As Dictionary(Of String, Dictionary(Of String, Object)), neutralLanguage As Dictionary(Of String, Object)) As Dictionary(Of String, List(Of String))
            Dim missing = New Dictionary(Of String, List(Of String))()
            For Each pair In resxDictionaries
                Dim resxs = pair.Value
                Dim list = New List(Of String)()
                For Each key In neutralLanguage.Keys
                    If Not resxs.ContainsKey(key) Then
                        list.Add(key)
                    End If
                Next
                If list.Count > 0 Then missing.Add(pair.Key, list)
            Next
            Return missing
        End Function

    End Class
End Namespace

ANd now my test looks like this.

vb.net
1
2
3
4
5
6
7
8
9
10
11
12
Imports NUnit.Framework                 
 
Namespace Resources
    Public Class TestReportResource
 
        <Test>
        Public Sub TestResx
            ResourceTester.TestResxForInconsistencies(Gettype(BI.My.Resources.Report), BI.My.Resources.Report.ResourceManager)
        End Sub
        
    End Class
End NameSpace
Imports NUnit.Framework                 

Namespace Resources
    Public Class TestReportResource

        <Test>
        Public Sub TestResx
            ResourceTester.TestResxForInconsistencies(Gettype(BI.My.Resources.Report), BI.My.Resources.Report.ResourceManager)
        End Sub
        
    End Class
End NameSpace

And when it failes it looks like this.

resx

So there you go, simple.