VerifyTranslatedStringFormatting.java 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. // run: java tools/VerifyTranslatedStringFormatting.java
  2. // Reads all localized strings and makes sure they contain valid formatting placeholders matching the original English strings to avoid crashes.
  3. import org.w3c.dom.*;
  4. import javax.xml.parsers.*;
  5. import java.io.*;
  6. import java.util.*;
  7. import java.util.regex.*;
  8. public class VerifyTranslatedStringFormatting{
  9. // %[argument_index$][flags][width][.precision][t]conversion
  10. private static final String formatSpecifier="%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
  11. private static final Pattern fsPattern=Pattern.compile(formatSpecifier);
  12. private static HashMap<String, List<String>> placeholdersInStrings=new HashMap<>();
  13. private static int errorCount=0;
  14. public static void main(String[] args) throws Exception{
  15. DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
  16. factory.setNamespaceAware(false);
  17. DocumentBuilder builder=factory.newDocumentBuilder();
  18. Document doc;
  19. try(FileInputStream in=new FileInputStream("mastodon/src/main/res/values/strings.xml")){
  20. doc=builder.parse(in);
  21. }
  22. NodeList list=doc.getDocumentElement().getChildNodes(); // why does this stupid NodeList thing exist at all?
  23. for(int i=0;i<list.getLength();i++){
  24. if(list.item(i) instanceof Element el){
  25. String name=el.getAttribute("name");
  26. String value;
  27. if("string".equals(el.getTagName())){
  28. value=el.getTextContent();
  29. }else if("plurals".equals(el.getTagName())){
  30. value=el.getElementsByTagName("item").item(0).getTextContent();
  31. }else{
  32. System.out.println("Warning: unexpected tag "+name);
  33. continue;
  34. }
  35. ArrayList<String> placeholders=new ArrayList<>();
  36. Matcher matcher=fsPattern.matcher(value);
  37. while(matcher.find()){
  38. placeholders.add(matcher.group());
  39. }
  40. placeholdersInStrings.put(name, placeholders);
  41. }
  42. }
  43. for(File file:new File("mastodon/src/main/res").listFiles()){
  44. if(file.getName().startsWith("values-")){
  45. File stringsXml=new File(file, "strings.xml");
  46. if(stringsXml.exists()){
  47. processFile(stringsXml);
  48. }
  49. }
  50. }
  51. if(errorCount>0){
  52. System.err.println("Found "+errorCount+" problems in localized strings");
  53. System.exit(1);
  54. }
  55. }
  56. private static void processFile(File file) throws Exception{
  57. DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
  58. factory.setNamespaceAware(false);
  59. DocumentBuilder builder=factory.newDocumentBuilder();
  60. Document doc;
  61. try(FileInputStream in=new FileInputStream(file)){
  62. doc=builder.parse(in);
  63. }
  64. NodeList list=doc.getDocumentElement().getChildNodes();
  65. for(int i=0;i<list.getLength();i++){
  66. if(list.item(i) instanceof Element el){
  67. String name=el.getAttribute("name");
  68. String value;
  69. if("string".equals(el.getTagName())){
  70. value=el.getTextContent();
  71. if(!verifyString(value, placeholdersInStrings.get(name))){
  72. errorCount++;
  73. System.out.println(file+": string "+name+" is missing placeholders");
  74. }
  75. }else if("plurals".equals(el.getTagName())){
  76. NodeList items=el.getElementsByTagName("item");
  77. for(int j=0;j<items.getLength();j++){
  78. Element item=(Element)items.item(j);
  79. value=item.getTextContent();
  80. String quantity=item.getAttribute("quantity");
  81. if(!verifyString(value, placeholdersInStrings.get(name))){
  82. // Some languages use zero/one/two for just these numbers so they may skip the placeholder
  83. // still make sure that there's no '%' characters to avoid crashes
  84. if(List.of("zero", "one", "two").contains(quantity) && !value.contains("%")){
  85. continue;
  86. }
  87. errorCount++;
  88. System.out.println(file+": string "+name+"["+quantity+"] is missing placeholders");
  89. }
  90. }
  91. }else{
  92. System.out.println("Warning: unexpected tag "+name);
  93. continue;
  94. }
  95. }
  96. }
  97. }
  98. private static boolean verifyString(String str, List<String> placeholders){
  99. if(placeholders==null)
  100. return true;
  101. for(String placeholder:placeholders){
  102. if(placeholder.equals("%,d")){
  103. // %,d and %d are interchangeable but %,d provides nicer formatting
  104. if(!str.contains(placeholder) && !str.contains("%d"))
  105. return false;
  106. }else if(!str.contains(placeholder)){
  107. return false;
  108. }
  109. }
  110. return true;
  111. }
  112. }