1 /* GNU gettext for C#
2  * Copyright (C) 2003-2004, 2007 Free Software Foundation, Inc.
3  * Written by Bruno Haible <bruno@clisp.org>, 2003.
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  */
18 
19 /*
20  * This program dumps a GettextResourceSet subclass (in a satellite assembly)
21  * or a .resources file as a PO file.
22  */
23 
24 using System; /* Object, String, Type, Console, Exception */
25 using System.Reflection; /* Assembly, MethodInfo, ConstructorInfo */
26 using System.Collections; /* Hashtable, DictionaryEntry */
27 using System.IO; /* BufferedStream, StreamWriter, TextWriter, FileNotFoundException, Path */
28 using System.Text; /* StringBuilder, UTF8Encoding */
29 using System.Resources; /* ResourceReader */
30 using GNU.Gettext; /* GettextResourceSet */
31 
32 namespace GNU.Gettext {
33   public class DumpResource {
34     private TextWriter Out;
DumpString(String str)35     private void DumpString (String str) {
36       int n = str.Length;
37       Out.Write('"');
38       for (int i = 0; i < n; i++) {
39         char c = str[i];
40         if (c == 0x0008) {
41           Out.Write('\\'); Out.Write('b');
42         } else if (c == 0x000c) {
43           Out.Write('\\'); Out.Write('f');
44         } else if (c == 0x000a) {
45           Out.Write('\\'); Out.Write('n');
46         } else if (c == 0x000d) {
47           Out.Write('\\'); Out.Write('r');
48         } else if (c == 0x0009) {
49           Out.Write('\\'); Out.Write('t');
50         } else if (c == '\\' || c == '"') {
51           Out.Write('\\'); Out.Write(c);
52         } else
53           Out.Write(c);
54       }
55       Out.Write('"');
56     }
DumpMessage(String msgid, String msgid_plural, Object msgstr)57     private void DumpMessage (String msgid, String msgid_plural, Object msgstr) {
58       int separatorPos = msgid.IndexOf('\u0004');
59       if (separatorPos >= 0) {
60         String msgctxt = msgid.Substring(0,separatorPos);
61         msgid = msgid.Substring(separatorPos+1);
62         Out.Write("msgctxt "); DumpString(msgctxt);
63       }
64       Out.Write("msgid "); DumpString(msgid); Out.Write('\n');
65       if (msgid_plural != null) {
66         Out.Write("msgid_plural "); DumpString(msgid_plural); Out.Write('\n');
67         for (int i = 0; i < (msgstr as String[]).Length; i++) {
68           Out.Write("msgstr[" + i + "] ");
69           DumpString((msgstr as String[])[i]);
70           Out.Write('\n');
71         }
72       } else {
73         Out.Write("msgstr "); DumpString(msgstr as String); Out.Write('\n');
74       }
75       Out.Write('\n');
76     }
77 
78     // ---------------- Dumping a GettextResourceSet ----------------
79 
Dump(GettextResourceSet catalog)80     private void Dump (GettextResourceSet catalog) {
81       MethodInfo pluralMethod =
82         catalog.GetType().GetMethod("GetMsgidPluralTable", Type.EmptyTypes);
83       // Search for the header entry.
84       {
85         Object header_entry = catalog.GetObject("");
86         // If there is no header entry, fake one.
87         // FIXME: This is not needed; right after po_lex_charset_init set
88         // the PO charset to UTF-8.
89         if (header_entry == null)
90           header_entry = "Content-Type: text/plain; charset=UTF-8\n";
91         DumpMessage("", null, header_entry);
92       }
93       // Now the other messages.
94       {
95         Hashtable plural = null;
96         if (pluralMethod != null)
97           plural = pluralMethod.Invoke(catalog, new Object[0]) as Hashtable;
98         foreach (String key in catalog.Keys)
99           if (!"".Equals(key)) {
100             Object value = catalog.GetObject(key);
101             String key_plural =
102               (plural != null && value is String[] ? plural[key] as String : null);
103             DumpMessage(key, key_plural, value);
104           }
105       }
106     }
107     // Essentially taken from class GettextResourceManager.
GetSatelliteAssembly(String baseDirectory, String resourceName, String cultureName)108     private static Assembly GetSatelliteAssembly (String baseDirectory, String resourceName, String cultureName) {
109       String satelliteExpectedLocation =
110         baseDirectory
111         + Path.DirectorySeparatorChar + cultureName
112         + Path.DirectorySeparatorChar + resourceName + ".resources.dll";
113       return Assembly.LoadFrom(satelliteExpectedLocation);
114     }
115     // Taken from class GettextResourceManager.
ConstructClassName(String resourceName)116     private static String ConstructClassName (String resourceName) {
117       // We could just return an arbitrary fixed class name, like "Messages",
118       // assuming that every assembly will only ever contain one
119       // GettextResourceSet subclass, but this assumption would break the day
120       // we want to support multi-domain PO files in the same format...
121       bool valid = (resourceName.Length > 0);
122       for (int i = 0; valid && i < resourceName.Length; i++) {
123         char c = resourceName[i];
124         if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
125               || (i > 0 && c >= '0' && c <= '9')))
126           valid = false;
127       }
128       if (valid)
129         return resourceName;
130       else {
131         // Use hexadecimal escapes, using the underscore as escape character.
132         String hexdigit = "0123456789abcdef";
133         StringBuilder b = new StringBuilder();
134         b.Append("__UESCAPED__");
135         for (int i = 0; i < resourceName.Length; i++) {
136           char c = resourceName[i];
137           if (c >= 0xd800 && c < 0xdc00
138               && i+1 < resourceName.Length
139               && resourceName[i+1] >= 0xdc00 && resourceName[i+1] < 0xe000) {
140             // Combine two UTF-16 words to a character.
141             char c2 = resourceName[i+1];
142             int uc = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
143             b.Append('_');
144             b.Append('U');
145             b.Append(hexdigit[(uc >> 28) & 0x0f]);
146             b.Append(hexdigit[(uc >> 24) & 0x0f]);
147             b.Append(hexdigit[(uc >> 20) & 0x0f]);
148             b.Append(hexdigit[(uc >> 16) & 0x0f]);
149             b.Append(hexdigit[(uc >> 12) & 0x0f]);
150             b.Append(hexdigit[(uc >> 8) & 0x0f]);
151             b.Append(hexdigit[(uc >> 4) & 0x0f]);
152             b.Append(hexdigit[uc & 0x0f]);
153             i++;
154           } else if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
155                        || (c >= '0' && c <= '9'))) {
156             int uc = c;
157             b.Append('_');
158             b.Append('u');
159             b.Append(hexdigit[(uc >> 12) & 0x0f]);
160             b.Append(hexdigit[(uc >> 8) & 0x0f]);
161             b.Append(hexdigit[(uc >> 4) & 0x0f]);
162             b.Append(hexdigit[uc & 0x0f]);
163           } else
164             b.Append(c);
165         }
166         return b.ToString();
167       }
168     }
169     // Essentially taken from class GettextResourceManager.
InstantiateResourceSet(Assembly satelliteAssembly, String resourceName, String cultureName)170     private static GettextResourceSet InstantiateResourceSet (Assembly satelliteAssembly, String resourceName, String cultureName) {
171       Type clazz = satelliteAssembly.GetType(ConstructClassName(resourceName)+"_"+cultureName.Replace('-','_'));
172       ConstructorInfo constructor = clazz.GetConstructor(Type.EmptyTypes);
173       return constructor.Invoke(null) as GettextResourceSet;
174     }
DumpResource(String baseDirectory, String resourceName, String cultureName)175     public DumpResource (String baseDirectory, String resourceName, String cultureName) {
176       // We are only interested in the messages belonging to the locale
177       // itself, not in the inherited messages. Therefore we instantiate just
178       // the GettextResourceSet, not a GettextResourceManager.
179       Assembly satelliteAssembly =
180         GetSatelliteAssembly(baseDirectory, resourceName, cultureName);
181       GettextResourceSet catalog =
182         InstantiateResourceSet(satelliteAssembly, resourceName, cultureName);
183       BufferedStream stream = new BufferedStream(Console.OpenStandardOutput());
184       Out = new StreamWriter(stream, new UTF8Encoding());
185       Dump(catalog);
186       Out.Close();
187       stream.Close();
188     }
189 
190     // ----------------- Dumping a .resources file ------------------
191 
DumpResource(String filename)192     public DumpResource (String filename) {
193       BufferedStream stream = new BufferedStream(Console.OpenStandardOutput());
194       Out = new StreamWriter(stream, new UTF8Encoding());
195       ResourceReader rr;
196       if (filename.Equals("-")) {
197         BufferedStream input = new BufferedStream(Console.OpenStandardInput());
198         // A temporary output stream is needed because ResourceReader expects
199         // to be able to seek in the Stream.
200         byte[] contents;
201         {
202           MemoryStream tmpstream = new MemoryStream();
203           byte[] buf = new byte[1024];
204           for (;;) {
205             int n = input.Read(buf, 0, 1024);
206             if (n == 0)
207               break;
208             tmpstream.Write(buf, 0, n);
209           }
210           contents = tmpstream.ToArray();
211           tmpstream.Close();
212         }
213         MemoryStream tmpinput = new MemoryStream(contents);
214         rr = new ResourceReader(tmpinput);
215       } else {
216         rr = new ResourceReader(filename);
217       }
218       foreach (DictionaryEntry entry in rr) // uses rr.GetEnumerator()
219         DumpMessage(entry.Key as String, null, entry.Value as String);
220       rr.Close();
221       Out.Close();
222       stream.Close();
223     }
224 
225     // --------------------------------------------------------------
226 
Main(String[] args)227     public static int Main (String[] args) {
228       try {
229         if (args.Length > 1)
230           new DumpResource(args[0], args[1], args[2]);
231         else
232           new DumpResource(args[0]);
233       } catch (Exception e) {
234         Console.Error.WriteLine(e);
235         Console.Error.WriteLine(e.StackTrace);
236         return 1;
237       }
238       return 0;
239     }
240   }
241 }
242