5 use YAML::Syck qw(Load LoadFile);
 
  12 locale-diff - Compare two YAML files and print how their datastructures differ
 
  16     # --keys is the default
 
  18     diff --keys en.yml is.yml
 
  20     # --untranslated-values compares prints keys whose values don't differ
 
  21     diff --untranslated-values-all en.yml is.yml
 
  23     # --untranslated-values-all compares prints keys whose values
 
  24     # don't differ. Ignoring the blacklist which prunes things
 
  25     # unlikley to be translated
 
  26     diff --untranslated-values-all en.yml is.yml
 
  28     # Check that interpolated variables ({{var}} and [[var]]) are the same
 
  29     diff --validate-variables en.yml is.yml
 
  33 This utility prints the differences between two YAML files using
 
  34 L<Test::Differences>. The purpose of it is to diff the files is
 
  35 F<config/locales> to find out what keys need to be added to the
 
  36 translated files when F<en.yml> changes.
 
  44 Print this help message.
 
  48 Show the hash keys that differ between the two files, useful merging
 
  49 new entries from F<en.yml> to a local file.
 
  51 =item --untranslated-values
 
  53 Show keys whose values are either exactly the same between the two
 
  54 files, or don't exist in the target file (the latter file
 
  55 specified). The values are pruned according to global and language
 
  56 specific blacklists found in the C<__DATA__> section of this script.
 
  58 This helps to find untranslated values.
 
  60 =item --untranslated-values-all
 
  62 Like C<--untranslated-values> but ignores blacklists.
 
  64 =item --validate-variables
 
  66 Check that interpolated Ruby i18n variables (C<{{foo}}> and
 
  67 C<[[foo]]>) are equivalent in the two provided files.
 
  73 E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avar@f-prot.com>
 
  77 # Get the command-line options
 
  78 Getopt::Long::Parser->new(
 
  79     config => [ qw< bundling no_ignore_case no_require_order pass_through > ],
 
  81     'h|help' => \my $help,
 
  83     'untranslated-values' => \my $untranslated_values,
 
  84     'untranslated-values-all' => \my $untranslated_values_all,
 
  85     'validate-variables' => \my $validate_variables,
 
  88 # --keys is the default
 
  89 $keys = 1 if not $untranslated_values_all and not $untranslated_values and not $validate_variables;
 
  94 # If we're not given two .yml files
 
  95 help() if @ARGV != 2 or (!-f $ARGV[0] or !-f $ARGV[1]);
 
  97 my ($from, $to) = @ARGV;
 
  99 my $from_data = LoadFile($from);
 
 100 my $to_data   = LoadFile($to);
 
 102 my $from_parsed = { iterate($from_data->{basename($from)}) };
 
 103 my $to_parsed = { iterate($to_data->{basename($to)}) };
 
 107     print_key_differences($from_parsed, $to_parsed);
 
 109 elsif ($untranslated_values or $untranslated_values_all)
 
 111     my @untranslated = untranslated_keys($from_parsed, $to_parsed);
 
 113     # Prune according to blacklist
 
 114     if ($untranslated_values) {
 
 115         @untranslated = prune_untranslated_with_blacklist(basename($to), @untranslated);
 
 118     say for @untranslated;
 
 119 } elsif ($validate_variables)
 
 121     print_validate_variables($from_parsed, $to_parsed);
 
 126 sub print_key_differences
 
 130     # Hack around Test::Differences wanting a Test::* module loaded
 
 132     sub Test::ok { print shift }
 
 135     eq_or_diff([ sort keys %$f ], [ sort keys %$t ]);
 
 138 sub untranslated_keys
 
 140     my ($from_parsed, $to_parsed) = @_;
 
 141     sort grep { not exists $to_parsed->{$_} or $from_parsed->{$_} eq $to_parsed->{$_} } keys %$from_parsed;
 
 144 sub prune_untranslated_with_blacklist
 
 146     my ($language, @keys) = @_;
 
 150     my $end_yaml = Load(join '', <DATA>);
 
 151     my $untranslated_values = $end_yaml->{untranslated_values};
 
 152     my $default = $untranslated_values->{default};
 
 153     my $this_language = $untranslated_values->{$language} || {};
 
 155     my %bw_list = (%$default, %$this_language);
 
 157     while (my ($key, $blacklisted) = each %bw_list)
 
 159         # FIXME: Does syck actually support true/false booleans in yaml?
 
 160         delete $keys{$key} if $blacklisted eq 'true'
 
 166 sub print_validate_variables
 
 170     while (my ($key, $val) = each %$f)
 
 172         next if exists $f->{$key} and not exists $t->{$key};
 
 174         my @from_var = parse_variables_from_string($f->{$key});
 
 175         my @to_var   = parse_variables_from_string($t->{$key});
 
 177         unless (@from_var ~~ @to_var) {
 
 178             say "$key in $from has (@from_var) and $to has (@to_var)";
 
 184 sub parse_variables_from_string
 
 188     # This probably matches most of the variables
 
 189     my $var = qr/ [a-z0-9_]+? /xs;
 
 191     if (my @var = $string =~ m/ \{\{ ($var) \}\} | \[\[ ($var) \]\] /gsx) {
 
 192         return sort grep { defined } @var;
 
 200     my ($hash, @path) = @_;
 
 203     while (my ($k, $v) = each %$hash)
 
 205         if (ref $v eq 'HASH')
 
 207              push @ret => iterate($v, @path, $k);
 
 211             push @ret => join(".",@path, $k), $v;
 
 221     $name =~ s[\..*?$][];
 
 229     Pod::Usage::pod2usage(
 
 230         -verbose => $arg{ verbose },
 
 231         -exitval => $arg{ exitval } || 0,
 
 238   # Default/Per language blacklist/whitelist for the
 
 239   # --untranslated-values switch. "true" as a value indicates that the
 
 240   # key is to be blacklisted, and "false" that it's to be
 
 241   # whitelisted. "false" is only required to whitelist a key
 
 242   # blacklisted by default on a per-language basis.
 
 246     layouts.intro_3_bytemark: true
 
 247     layouts.intro_3_ucl: true
 
 248     layouts.project_name.h1: true
 
 249     layouts.project_name.title: true
 
 250     site.index.license.project_url: true
 
 252     activerecord.attributes.message.sender: true
 
 253     activerecord.attributes.trace.name: true
 
 254     activerecord.models.changeset: true
 
 255     activerecord.models.relation: true
 
 256     browse.changeset.changeset: true
 
 257     browse.changeset.changesetxml: true
 
 258     browse.changeset.osmchangexml: true
 
 259     browse.changeset.title: true
 
 260     browse.common_details.version: true
 
 261     browse.containing_relation.relation: true
 
 262     browse.relation.relation: true
 
 263     browse.relation.relation_title: true
 
 264     browse.start_rjs.details: true
 
 265     browse.start_rjs.object_list.details: true
 
 266     browse.tag_details.tags: true
 
 267     changeset.changesets.id: true
 
 268     export.start.export_button: true
 
 269     export.start.format: true
 
 270     export.start.output: true
 
 271     export.start.zoom: true
 
 272     export.start_rjs.export: true
 
 275     site.edit.anon_edits: true
 
 276     site.index.license.license_name: true
 
 277     site.index.permalink: true
 
 278     site.key.table.entry.park: true
 
 279     site.search.submit_text: true
 
 280     trace.edit.tags: true
 
 282     trace.trace_form.tags: true
 
 283     trace.trace_optionals.tags: true
 
 284     trace.view.tags: true
 
 285     user.account.public editing.enabled link: true