• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.tool;
2 
3 import java.io.File;
4 import java.io.FileNotFoundException;
5 import java.io.IOException;
6 import java.io.PrintWriter;
7 import java.io.UncheckedIOException;
8 import java.util.Arrays;
9 import java.util.Collection;
10 import java.util.HashSet;
11 import java.util.LinkedHashSet;
12 import java.util.Map;
13 import java.util.Map.Entry;
14 import java.util.Objects;
15 import java.util.Set;
16 import java.util.TreeSet;
17 import java.util.regex.Matcher;
18 import java.util.regex.Pattern;
19 
20 import com.google.common.collect.HashMultimap;
21 import com.google.common.collect.ImmutableSet;
22 import com.google.common.collect.Multimap;
23 import com.google.common.collect.Sets;
24 import com.google.common.collect.TreeMultimap;
25 import com.google.common.io.Files;
26 import com.ibm.icu.util.Output;
27 
28 import org.unicode.cldr.tool.Option.Options;
29 import org.unicode.cldr.tool.Option.Params;
30 import org.unicode.cldr.util.CLDRConfig;
31 import org.unicode.cldr.util.CLDRFile;
32 import org.unicode.cldr.util.CLDRPaths;
33 import org.unicode.cldr.util.CldrUtility;
34 import org.unicode.cldr.util.DtdType;
35 import org.unicode.cldr.util.Factory;
36 import org.unicode.cldr.util.Level;
37 import org.unicode.cldr.util.LocaleIDParser;
38 import org.unicode.cldr.util.LogicalGrouping;
39 import org.unicode.cldr.util.Pair;
40 import org.unicode.cldr.util.SupplementalDataInfo;
41 import org.unicode.cldr.util.XMLSource;
42 import org.unicode.cldr.util.XPathParts;
43 
44 public class GenerateProductionData {
45     static boolean DEBUG = false;
46     static boolean VERBOSE = false;
47     static Matcher FILE_MATCH = null;
48 
49     static String SOURCE_COMMON_DIR = null;
50     static String DEST_COMMON_DIR = null;
51 
52     static boolean ADD_LOGICAL_GROUPS = false;
53     static boolean ADD_DATETIME = false;
54     static boolean ADD_SIDEWAYS = false;
55     static boolean ADD_ROOT = false;
56     static boolean INCLUDE_COMPREHENSIVE = false;
57     static boolean CONSTRAINED_RESTORATION = false;
58 
59     static final Set<String> NON_XML = ImmutableSet.of("dtd", "properties", "testData", "uca");
60     static final Set<String> COPY_ANYWAY = ImmutableSet.of("casing", "collation"); // don't want to "clean up", makes format difficult to use
61     static final SupplementalDataInfo SDI = CLDRConfig.getInstance().getSupplementalDataInfo();
62 
63     static final Multimap<String, Pair<String, String>> localeToSubdivisionsToMigrate = TreeMultimap.create();
64 
65     enum MyOptions {
66         sourceDirectory(new Params()
67             .setHelp("source common directory")
68             .setDefault(CLDRPaths.COMMON_DIRECTORY)
69             .setMatch(".*")),
70         destinationDirectory(new Params()
71             .setHelp("destination common directory")
72             .setDefault(CLDRPaths.STAGING_DIRECTORY + "production/common")
73             .setMatch(".*")),
74         logicalGroups(new Params()
75             .setHelp("add path/values for logical groups")
76             .setDefault("true")
77             .setMatch("true|false")),
78         time(new Params()
79             .setHelp("add path/values for stock date/time/datetime")
80             .setDefault("true")
81             .setMatch("true|false")),
82         Sideways(new Params()
83             .setHelp("add path/values for sideways inheritance")
84             .setDefault("true")
85             .setMatch("true|false")),
86         root(new Params()
87             .setHelp("add path/values for root and code-fallback")
88             .setDefault("true")
89             .setMatch("true|false")),
90         constrainedRestoration(new Params()
91             .setHelp("only add inherited paths that were in original file")
92             .setDefault("true")
93             .setMatch("true|false")),
94         includeComprehensive(new Params()
95             .setHelp("exclude comprehensive paths — otherwise just to modern level")
96             .setDefault("true")
97             .setMatch("true|false")),
98         verbose(new Params()
99             .setHelp("verbose debugging messages")),
100         Debug(new Params()
101             .setHelp("debug")),
102         fileMatch(new Params()
103             .setHelp("regex to match patterns")
104             .setMatch(".*")),
105         ;
106 
107         // BOILERPLATE TO COPY
108         final Option option;
109 
MyOptions(Params params)110         private MyOptions(Params params) {
111             option = new Option(this, params);
112         }
113 
114         private static Options myOptions = new Options();
115         static {
116             for (MyOptions option : MyOptions.values()) {
myOptions.add(option, option.option)117                 myOptions.add(option, option.option);
118             }
119         }
120 
parse(String[] args, boolean showArguments)121         private static Set<String> parse(String[] args, boolean showArguments) {
122             return myOptions.parse(MyOptions.values()[0], args, true);
123         }
124     }
125 
main(String[] args)126     public static void main(String[] args) {
127         // TODO rbnf and segments don't have modern coverage; fix there.
128 
129         MyOptions.parse(args, true);
130         SOURCE_COMMON_DIR = MyOptions.sourceDirectory.option.getValue();
131         DEST_COMMON_DIR = MyOptions.destinationDirectory.option.getValue();
132 
133         // debugging
134         VERBOSE = MyOptions.verbose.option.doesOccur();
135         DEBUG = MyOptions.Debug.option.doesOccur();
136         String fileMatch = MyOptions.fileMatch.option.getValue();
137         if (fileMatch != null) {
138             FILE_MATCH = Pattern.compile(fileMatch).matcher("");
139         }
140 
141         // controls for minimization
142         ADD_LOGICAL_GROUPS = "true".equalsIgnoreCase(MyOptions.logicalGroups.option.getValue());
143         ADD_DATETIME = "true".equalsIgnoreCase(MyOptions.time.option.getValue());
144         ADD_SIDEWAYS = "true".equalsIgnoreCase(MyOptions.Sideways.option.getValue());
145         ADD_ROOT = "true".equalsIgnoreCase(MyOptions.root.option.getValue());
146 
147         // constraints
148         INCLUDE_COMPREHENSIVE = "true".equalsIgnoreCase(MyOptions.includeComprehensive.option.getValue());
149         CONSTRAINED_RESTORATION = "true".equalsIgnoreCase(MyOptions.constrainedRestoration.option.getValue());
150 
151         // get directories
152 
153         Arrays.asList(DtdType.values()).parallelStream()
154             .unordered()
155             .forEach(type -> {
156             boolean isLdmlDtdType = type == DtdType.ldml;
157 
158             // bit of a hack, using the ldmlICU — otherwise unused! — to get the nonXML files.
159             Set<String> directories = (type == DtdType.ldmlICU) ? NON_XML : type.directories;
160 
161             for (String dir : directories) {
162                 File sourceDir = new File(SOURCE_COMMON_DIR, dir);
163                 File destinationDir = new File(DEST_COMMON_DIR, dir);
164                 Stats stats = new Stats();
165                 copyFilesAndReturnIsEmpty(sourceDir, destinationDir, null, isLdmlDtdType, stats);
166             }
167         });
168         if (!localeToSubdivisionsToMigrate.isEmpty()) {
169             System.err.println("WARNING: Subdivision files not written");
170             for (Entry<String, Pair<String, String>> entry : localeToSubdivisionsToMigrate.entries()) {
171                 System.err.println(entry.getKey() + " \t" + entry.getValue());
172             }
173         }
174     }
175 
176     private static class Stats {
177         long files;
178         long removed;
179         long retained;
180         long remaining;
clear()181         Stats clear() {
182             files = removed = retained = remaining = 0;
183             return this;
184         }
185         @Override
toString()186         public String toString() {
187             return
188                 "files=" + files
189                 + (removed + retained + remaining == 0 ? ""
190                     : "; removed=" + removed
191                     + "; retained=" + retained
192                     + "; remaining=" + remaining);
193         }
showNonZero(String label)194         public void showNonZero(String label) {
195             if (removed + retained + remaining != 0) {
196                 System.out.println(label + toString());
197             }
198         }
199     }
200 
201     /**
202      * Copy files in directories, recursively.
203      * @param sourceFile
204      * @param destinationFile
205      * @param factory
206      * @param isLdmlDtdType
207      * @param stats
208      * @param hasChildren
209      * @return true if the file is an ldml file with empty content.
210      */
copyFilesAndReturnIsEmpty(File sourceFile, File destinationFile, Factory factory, boolean isLdmlDtdType, final Stats stats)211     private static boolean copyFilesAndReturnIsEmpty(File sourceFile, File destinationFile,
212         Factory factory, boolean isLdmlDtdType, final Stats stats) {
213         if (sourceFile.isDirectory()) {
214 
215             System.out.println(sourceFile + " => " + destinationFile);
216             if (!destinationFile.mkdirs()) {
217                 // if created, remove old contents
218                 Arrays.stream(destinationFile.listFiles()).forEach(File::delete);
219             }
220 
221             Set<String> sorted = new TreeSet<>();
222             sorted.addAll(Arrays.asList(sourceFile.list()));
223 
224             if (COPY_ANYWAY.contains(sourceFile.getName())) { // special cases
225                 isLdmlDtdType = false;
226             }
227             // reset factory for directory
228             factory = null;
229             if (isLdmlDtdType) {
230                 // if the factory is empty, then we just copy files
231                 factory = Factory.make(sourceFile.toString(), ".*");
232             }
233             boolean isMainDir = factory != null && sourceFile.getName().contentEquals("main");
234             boolean isRbnfDir = factory != null && sourceFile.getName().contentEquals("rbnf");
235 
236             Set<String> emptyLocales = new HashSet<>();
237             final Stats stats2 = new Stats();
238             final Factory theFactory = factory;
239             final boolean isLdmlDtdType2 = isLdmlDtdType;
240             sorted
241                 .parallelStream()
242                 .forEach(file -> {
243                     File sourceFile2 = new File(sourceFile, file);
244                     File destinationFile2 = new File(destinationFile, file);
245                     if (VERBOSE) System.out.println("\t" + file);
246 
247                     // special step to just copy certain files like main/root.xml file
248                     Factory currFactory = theFactory;
249                     if (isMainDir) {
250                         if (file.equals("root.xml")) {
251                             currFactory = null;
252                         }
253                     } else if (isRbnfDir) {
254                         currFactory = null;
255                     }
256 
257                     // when the currFactory is null, we just copy files as-is
258                     boolean isEmpty = copyFilesAndReturnIsEmpty(sourceFile2, destinationFile2, currFactory, isLdmlDtdType2, stats2);
259                     if (isEmpty) { // only happens for ldml
260                         emptyLocales.add(file.substring(0,file.length()-4)); // remove .xml for localeId
261                     }
262                 });
263             stats2.showNonZero("\tTOTAL:\t");
264             // if there are empty ldml files, AND we aren't in /main/,
265             // then remove any without children
266             if (!emptyLocales.isEmpty() && !sourceFile.getName().equals("main")) {
267                 Set<String> childless = getChildless(emptyLocales, factory.getAvailable());
268                 if (!childless.isEmpty()) {
269                     if (VERBOSE) System.out.println("\t" + destinationFile + "\tRemoving empty locales:" + childless);
270                     childless.stream().forEach(locale -> new File(destinationFile, locale + ".xml").delete());
271                 }
272             }
273             return false;
274         } else if (factory != null) {
275             String file = sourceFile.getName();
276             if (!file.endsWith(".xml")) {
277                 return false;
278             }
279             String localeId = file.substring(0, file.length()-4);
280             if (FILE_MATCH != null) {
281                 if (!FILE_MATCH.reset(localeId).matches()) {
282                     return false;
283                 }
284             }
285             boolean isRoot = localeId.equals("root");
286             String directoryName = sourceFile.getParentFile().getName();
287             boolean isSubdivisionDirectory = "subdivisions".equals(directoryName);
288 
289             CLDRFile cldrFileUnresolved = factory.make(localeId, false);
290             CLDRFile cldrFileResolved = factory.make(localeId, true);
291             boolean gotOne = false;
292             Set<String> toRemove = new TreeSet<>(); // TreeSet just makes debugging easier
293             Set<String> toRetain = new TreeSet<>();
294             Output<String> pathWhereFound = new Output<>();
295             Output<String> localeWhereFound = new Output<>();
296 
297             boolean isArabicSpecial = localeId.equals("ar") || localeId.startsWith("ar_");
298 
299             String debugPath = null; // "//ldml/units/unitLength[@type=\"short\"]/unit[@type=\"power-kilowatt\"]/displayName";
300             String debugLocale = "af";
301 
302             for (String xpath : cldrFileUnresolved) {
303                 if (xpath.startsWith("//ldml/identity")) {
304                     continue;
305                 }
306                 if (debugPath != null && localeId.equals(debugLocale) && xpath.equals(debugPath)) {
307                     int debug = 0;
308                 }
309 
310                 String value = cldrFileUnresolved.getStringValue(xpath);
311                 if (value == null || CldrUtility.INHERITANCE_MARKER.equals(value)) {
312                     toRemove.add(xpath);
313                     continue;
314                 }
315 
316                 // special-case the root values that are only for Survey Tool use
317 
318                 if (isRoot) {
319                     if (xpath.startsWith("//ldml/annotations/annotation")) {
320                         toRemove.add(xpath);
321                         continue;
322                     }
323                 }
324 
325                 // special case for Arabic defaultNumberingSystem
326                 if (isArabicSpecial && xpath.contains("/defaultNumberingSystem")) {
327                     toRetain.add(xpath);
328                 }
329 
330                 // remove items that are the same as their bailey values. This also catches Inheritance Marker
331 
332                 String bailey = cldrFileResolved.getConstructedBaileyValue(xpath, pathWhereFound, localeWhereFound);
333                 if (value.equals(bailey)
334                     && (!ADD_SIDEWAYS
335                         || pathEqualsOrIsAltVariantOf(xpath, pathWhereFound.value))
336                     && (!ADD_ROOT
337                         || (!Objects.equals(XMLSource.ROOT_ID, localeWhereFound.value)
338                             && !Objects.equals(XMLSource.CODE_FALLBACK_ID, localeWhereFound.value)))) {
339                     toRemove.add(xpath);
340                     continue;
341                 }
342 
343                 // Move subdivisions elsewhere
344                 if (!isSubdivisionDirectory && xpath.startsWith("//ldml/localeDisplayNames/subdivisions/subdivision")) {
345                     localeToSubdivisionsToMigrate.put(localeId, Pair.of(xpath, value));
346                     toRemove.add(xpath);
347                     continue;
348                 }
349                 // remove level=comprehensive (under setting)
350 
351                 if (!INCLUDE_COMPREHENSIVE) {
352                     Level coverage = SDI.getCoverageLevel(xpath, localeId);
353                     if (coverage == Level.COMPREHENSIVE) {
354                         toRemove.add(xpath);
355                         continue;
356                     }
357                 }
358 
359                 // if we got all the way to here, we have a non-empty result
360 
361                 // check to see if we might need to flesh out logical groups
362                 // TODO Should be done in the converter tool!!
363                 if (ADD_LOGICAL_GROUPS && !LogicalGrouping.isOptional(cldrFileResolved, xpath)) {
364                     Set<String> paths = LogicalGrouping.getPaths(cldrFileResolved, xpath);
365                     if (paths != null && paths.size() > 1) {
366                         for (String possiblePath : paths) {
367                             // Unclear from API whether we need to do this filtering
368                             if (!LogicalGrouping.isOptional(cldrFileResolved, possiblePath)) {
369                                 toRetain.add(possiblePath);
370                             }
371                         }
372                     }
373                 }
374 
375                 // check to see if we might need to flesh out datetime.
376                 // TODO Should be done in the converter tool!!
377                 if (ADD_DATETIME && isDateTimePath(xpath)) {
378                     toRetain.addAll(dateTimePaths(xpath));
379                 }
380 
381                 // past the gauntlet
382                 gotOne = true;
383             }
384 
385             // we even add empty files, but can delete them back on the directory level.
386             try (PrintWriter pw = new PrintWriter(destinationFile)) {
387                 CLDRFile outCldrFile = cldrFileUnresolved.cloneAsThawed();
388                 if (isSubdivisionDirectory) {
389                     Collection<Pair<String, String>> path_values = localeToSubdivisionsToMigrate.get(localeId);
390                     if (path_values != null) {
391                         for (Pair<String, String>path_value : path_values) {
392                             outCldrFile.add(path_value.getFirst(), path_value.getSecond());
393                         }
394                         localeToSubdivisionsToMigrate.removeAll(localeId);
395                     }
396                 }
397 
398                 // Remove paths, but pull out the ones to retain
399                 // example:
400                 // toRemove == {a b c} // c may have ^^^ value
401                 // toRetain == {b c d} // d may have ^^^ value
402 
403                 if (DEBUG) {
404                     showIfNonZero(localeId, "removing", toRemove);
405                     showIfNonZero(localeId, "retaining", toRetain);
406 
407                 }
408                 if (CONSTRAINED_RESTORATION) {
409                     toRetain.retainAll(toRemove); // only add paths that were there already
410                     // toRetain == {b c}
411                     if (DEBUG) {
412                         showIfNonZero(localeId, "constrained retaining", toRetain);
413                     }
414                 }
415 
416                 boolean changed0 = toRemove.removeAll(toRetain);
417                 // toRemove == {a}
418                 if (DEBUG && changed0) {
419                     showIfNonZero(localeId, "final removing", toRemove);
420                 }
421 
422                 boolean changed = toRetain.removeAll(toRemove);
423                 // toRetain = {b c d} or if constrained, {b c}
424                 if (DEBUG && changed) {
425                     showIfNonZero(localeId, "final retaining", toRetain);
426                 }
427 
428                 outCldrFile.removeAll(toRemove, false);
429                 if (DEBUG) {
430                     for (String xpath : toRemove) {
431                         System.out.println(localeId + ": removing: «"
432                             + cldrFileUnresolved.getStringValue(xpath)
433                             + "», " + xpath);
434                     }
435                 }
436 
437                 // now set any null values to bailey values if not present
438                 for (String xpath : toRetain) {
439                     if (debugPath != null && localeId.equals(debugLocale) && xpath.equals(debugPath)) {
440                         int debug = 0;
441                     }
442                     String value = cldrFileResolved.getStringValue(xpath);
443                     if (value == null || value.equals(CldrUtility.INHERITANCE_MARKER)) {
444                         throw new IllegalArgumentException(localeId + ": " + value + " in value for " + xpath);
445                     } else {
446                         if (DEBUG) {
447                             String oldValue = cldrFileUnresolved.getStringValue(xpath);
448                             System.out.println("Restoring: «" + oldValue + "» ⇒ «" + value
449                                 + "»\t" + xpath);
450                         }
451                         outCldrFile.add(xpath, value);
452                     }
453                 }
454 
455                 // double-check results
456                 int count = 0;
457                 for (String xpath : outCldrFile) {
458                     if (debugPath != null && localeId.equals(debugLocale) && xpath.equals(debugPath)) {
459                         int debug = 0;
460                     }
461                     String value = outCldrFile.getStringValue(xpath);
462                     if (value == null || value.equals(CldrUtility.INHERITANCE_MARKER)) {
463                         throw new IllegalArgumentException(localeId + ": " + value + " in value for " + xpath);
464                     }
465                 }
466 
467                 outCldrFile.write(pw);
468                 ++stats.files;
469                 stats.removed += toRemove.size();
470                 stats.retained += toRetain.size();
471                 stats.remaining += count;
472             } catch (FileNotFoundException e) {
473                 throw new UncheckedIOException("Can't copy " + sourceFile + " to " + destinationFile + " — ", e);
474             }
475             return !gotOne;
476         } else {
477             if (FILE_MATCH != null) {
478                 String file = sourceFile.getName();
479                 int dotPos = file.lastIndexOf('.');
480                 String baseName = dotPos >= 0 ? file.substring(0, file.length()-dotPos) : file;
481                 if (!FILE_MATCH.reset(baseName).matches()) {
482                     return false;
483                 }
484             }
485             // for now, just copy
486             ++stats.files;
487             copyFiles(sourceFile, destinationFile);
488             return false;
489         }
490     }
491 
showIfNonZero(String localeId, String title, Set<String> toRemove)492     private static void showIfNonZero(String localeId, String title, Set<String> toRemove) {
493         if (toRemove.size() != 0) {
494             System.out.println(localeId + ": "
495                 + title
496                 + ": " + toRemove.size());
497         }
498     }
499 
pathEqualsOrIsAltVariantOf(String desiredPath, String foundPath)500     private static boolean pathEqualsOrIsAltVariantOf(String desiredPath, String foundPath) {
501         if (desiredPath.equals(foundPath)) {
502             return true;
503         }
504         if (desiredPath.contains("type=\"en_GB\"") && desiredPath.contains("alt=")) {
505             int debug = 0;
506         }
507         if (foundPath == null) {
508             // We can do this, because the bailey value has already been checked
509             // Since it isn't null, a null indicates a constructed alt value
510             return true;
511         }
512         XPathParts desiredPathParts = XPathParts.getFrozenInstance(desiredPath);
513         XPathParts foundPathParts = XPathParts.getFrozenInstance(foundPath);
514         if (desiredPathParts.size() != foundPathParts.size()) {
515             return false;
516         }
517         for (int e = 0; e < desiredPathParts.size(); ++e) {
518             String element1 = desiredPathParts.getElement(e);
519             String element2 = foundPathParts.getElement(e);
520             if (!element1.equals(element2)) {
521                 return false;
522             }
523             Map<String, String> attr1 = desiredPathParts.getAttributes(e);
524             Map<String, String> attr2 = foundPathParts.getAttributes(e);
525             if (attr1.equals(attr2)) {
526                 continue;
527             }
528             Set<String> keys1 = attr1.keySet();
529             Set<String> keys2 = attr2.keySet();
530             for (String attr : Sets.union(keys1, keys2)) {
531                 if (attr.equals("alt")) {
532                     continue;
533                 }
534                 if (!Objects.equals(attr1.get(attr), attr2.get(attr))) {
535                     return false;
536                 }
537             }
538         }
539         return true;
540     }
541 
isDateTimePath(String xpath)542     private static boolean isDateTimePath(String xpath) {
543         return xpath.startsWith("//ldml/dates/calendars/calendar")
544             && xpath.contains("FormatLength[@type=");
545     }
546 
547     /** generate full dateTimePaths from any element
548     //ldml/dates/calendars/calendar[@type="gregorian"]/dateFormats/dateFormatLength[@type=".*"]/dateFormat[@type="standard"]/pattern[@type="standard"]
549     //ldml/dates/calendars/calendar[@type="gregorian"]/timeFormats/timeFormatLength[@type=".*"]/timeFormat[@type="standard"]/pattern[@type="standard"]
550     //ldml/dates/calendars/calendar[@type="gregorian"]/dateTimeFormats/dateTimeFormatLength[@type=".*"]/dateTimeFormat[@type="standard"]/pattern[@type="standard"]
551      */
dateTimePaths(String xpath)552     private static Set<String> dateTimePaths(String xpath) {
553         LinkedHashSet<String> result = new LinkedHashSet<>();
554         String prefix = xpath.substring(0,xpath.indexOf(']') + 2); // get after ]/
555         for (String type : Arrays.asList("date", "time", "dateTime")) {
556             String pattern = prefix + "$XFormats/$XFormatLength[@type=\"$Y\"]/$XFormat[@type=\"standard\"]/pattern[@type=\"standard\"]".replace("$X", type);
557             for (String width : Arrays.asList("full", "long", "medium", "short")) {
558                 result.add(pattern.replace("$Y", width));
559             }
560         }
561         return result;
562     }
563 
getChildless(Set<String> emptyLocales, Set<String> available)564     private static Set<String> getChildless(Set<String> emptyLocales, Set<String> available) {
565         // first build the parent2child map
566         Multimap<String,String> parent2child = HashMultimap.create();
567         for (String locale : available) {
568             String parent = LocaleIDParser.getParent(locale);
569             if (parent != null) {
570                 parent2child.put(parent, locale);
571             }
572         }
573 
574         // now cycle through the empties
575         Set<String> result = new HashSet<>();
576         for (String empty : emptyLocales) {
577             if (allChildrenAreEmpty(empty, emptyLocales, parent2child)) {
578                 result.add(empty);
579             }
580         }
581         return result;
582     }
583 
584     /**
585      * Recursively checks that all children are empty (including that there are no children)
586      * @param name
587      * @param emptyLocales
588      * @param parent2child
589      * @return
590      */
allChildrenAreEmpty( String locale, Set<String> emptyLocales, Multimap<String, String> parent2child)591     private static boolean allChildrenAreEmpty(
592         String locale,
593         Set<String> emptyLocales,
594         Multimap<String, String> parent2child) {
595 
596         Collection<String> children = parent2child.get(locale);
597         for (String child : children) {
598             if (!emptyLocales.contains(child)) {
599                 return false;
600             }
601             if (!allChildrenAreEmpty(child, emptyLocales, parent2child)) {
602                 return false;
603             }
604         }
605         return true;
606     }
607 
copyFiles(File sourceFile, File destinationFile)608     private static void copyFiles(File sourceFile, File destinationFile) {
609         try {
610             Files.copy(sourceFile, destinationFile);
611         } catch (IOException e) {
612             System.err.println("Can't copy " + sourceFile + " to " + destinationFile + " — " + e);
613         }
614     }
615 }
616