#! /usr/bin/perl # Validate a .mse-locale file by checking that the keys match those used in the source code, and that they have the right number of arguments. use strict; use File::Find; # ------------------------------------------------------------------------------ # Step 0: arguments # ------------------------------------------------------------------------------ if (scalar @ARGV < 1) { die "Usage: $0 [...]" } my $indir = shift @ARGV; # ------------------------------------------------------------------------------ # Step 1: find keys used in source code # ------------------------------------------------------------------------------ # Determine the keys that should be in the locale file, # and the number of arguments the keys should have # Array of locale keys: maps type -> key -> info # where info is {argc:arity, opt:optional(only used in commented code)} our %locale_keys; our $i=0; sub arg_count { return scalar split(/,/,$_[0]); } sub make_comment { my $input = $_[0]; $input =~ s/(_[A-Z])/_COMMENT$1/g; return $input; } sub normalize { my $key = shift; $key =~ s/_/ /g; return $key; } # for each .cpp/.hpp file, collect locale calls sub gather_locale_keys { my $filename = $_; my $full_name = $File::Find::name; if (!($filename =~ /\..pp$/)) { return; } # Read file open(my $fh, "<", $filename) or die "Failed to open source file $full_name"; my $body = join('',<$fh>); close $fh; # Custom argument expansion my $inparen; $inparen = qr/[^()]|\((??{$inparen})*\)/; # recursive paren matching $body =~ s/(?:String::Format|format_string)\((_[A-Z]+)(_\([^)]+\)),($inparen+)/ $1 . "_" . arg_count($3) . $2 /ge; $body =~ s/action_name_for[(][^,]*,\s*(_[A-Z]+)(_\([^)]+\))/$1_1$2/g; # Drop comments, mark found items as 'optional' $body =~ s{//[^\n]*} {find_locale_calls($&, 1)}ge; $body =~ s{/\*.*?\*/}{find_locale_calls($&, 1)}ge; find_locale_calls($body, 0); } sub find_locale_calls { my $body = shift; my $in_comment = shift; # Find calls to locale functions while ($body =~ /_(COMMENT_)?(MENU|HELP|TOOL|TOOLTIP|LABEL|BUTTON|TITLE|TYPE|ACTION|ERROR)_(?:([1-9])_)?\(\s*\"([^\"]+)\"/g) { my $type = $2; my $argc = $3 ? $3 : 0; my $key = normalize($4); if (defined($locale_keys{$type}{$key}{'argc'}) && $locale_keys{$type}{$key}{'argc'} != $argc) { die "ERROR: locale key _${type}_($key) used with different arities"; } $locale_keys{$type}{$key}{'opt'} = defined($locale_keys{$type}{$key}{'opt'}) ? ($locale_keys{$type}{$key}{'opt'} && $in_comment) : $in_comment; $locale_keys{$type}{$key}{'argc'} = $argc; } # addPanel/add_menu_tr/add_tool_tr use multiple locale types my $ARG = qr{[^,]+}; my $STRARG = qr{\s*(?:_\()?\"([^\"]+)\"\)?}; find_multi_locale_calls($body, $in_comment, qr{addPanel\s*\((?:$ARG,){6} $STRARG \)}x, ["MENU","HELP","TOOL","TOOLTIP"]); find_multi_locale_calls($body, $in_comment, qr{menu_item_tr\s*\((?:$ARG,){3} $STRARG }x, ["MENU","HELP"]); while ($body =~ m{add_tool_tr\s*\((?:$ARG,){3} $STRARG \s*,?\s*(true|false)?}gx) { if ($2 eq 'true') { add_locale_keys(["TOOL","TOOLTIP","HELP"], normalize($1), $in_comment); } else { add_locale_keys(["TOOLTIP","HELP"], normalize($1), $in_comment); } } } sub add_locale_keys { my $types = shift; my $key = shift; my $in_comment = shift; foreach my $type (@{$types}) { $locale_keys{$type}{$key}{'opt'} = $in_comment; $locale_keys{$type}{$key}{'argc'} = 0; } } sub find_multi_locale_calls { my $body = shift; my $in_comment = shift; my $re = shift; my $types = shift; while ($body =~ m{$re}g) { my $key = normalize($1); add_locale_keys($types, $key, $in_comment); } } #my $filename = 'src/code_template.cpp'; #open(my $fh, "<", $filename) or die "WTF Failed to open source file $filename".length($filename); find(\&gather_locale_keys, $indir); if (scalar(%locale_keys) == 0) { die "Did not find any source files with locale keys" } print $locale_keys{'ERROR'}{'successful instalal'} . "\n"; # ------------------------------------------------------------------------------ # Step 2: validate a locale # ------------------------------------------------------------------------------ sub parse_locale { # Load and parse a .mse-locale file # Return a mapping type -> key -> value my $locale_file = shift; open(my $fh, "<", "$locale_file/locale") or die "Error: locale file not found: $locale_file/locale"; # Get lines from file my $type = undef; my $key = undef; my %locale; for my $line (<$fh>) { if ($line =~ /^\s*#|^\s*$/) { # comment } elsif ($line =~ /^([^:\t]+):/) { $type = uc $1; $key = undef; } elsif ($line =~ /^\t([^:\t]+):(.*)/) { $key = normalize($1); if (defined($locale{$type}{$key})) { die "Locale key already defined: $type: $key"; } $locale{$type}{$key} = $2; } elsif ($line =~ /^\t\t(.*)/) { $locale{$type}{$key} .= $1; } else { die "Unknown line in locale file: $line\n"; } } close $fh; return %locale } sub validate_locale { my $locale_file = shift; my %locale = parse_locale($locale_file); # Validate locale: # The set of keys should match exactly, so every key expected by the program should be in the locale file, and vice-versa my $ok = 1; foreach my $type (sort keys %locale_keys) { if (!defined($locale{$type})) { print "Missing key in locale: $type\n have keys " . "[" . join(', ', keys %locale) . "]\n"; $ok = 0; next; } foreach my $key (sort keys %{$locale_keys{$type}}) { if (!defined($locale{$type}{$key})) { if (!$locale_keys{$type}{$key}{'opt'}) { print "Missing key in locale: $type: '$key'\n"; $ok = 0; } next; } # Count the number of printf style arguments in the value my @args = $locale{$type}{$key} =~ /%[sd]/g; my $argc = scalar(@args); if ($argc != $locale_keys{$type}{$key}{'argc'}) { print "Incorrect number of arguments for $type: $key. Expected $locale_keys{$type}{$key}{'argc'}, got $argc\n"; $ok = 0; } } } foreach my $type (sort keys %locale) { next if $type eq 'PACKAGE'; # Ignore package specific locale keys if (!defined($locale_keys{$type})) { print "Unknown key in locale: $type\n expected keys " . "[" . join(', ', keys %locale_keys) . "]\n"; $ok = 0; next; } foreach my $key (sort keys %{$locale{$type}}) { if (!defined($locale_keys{$type}{$key})) { print "Unknown key in locale: $type: '$key'\n"; $ok = 0; } } } return $ok; } if (scalar @ARGV < 1) { die "No locales found to validate"; } for my $locale (@ARGV) { if (!validate_locale($locale)) { exit 1; } }