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