removed license_url from blacklist
[rails.git] / script / locale / diff
1 #!/usr/bin/env perl
2 use strict;
3 use warnings;
4 use YAML::Syck qw(Load LoadFile);
5 use Test::Differences;
6 use Pod::Usage ();
7 use Getopt::Long ();
8
9 =head1 NAME
10
11 locale-diff - Compare two YAML files and print how their datastructures differ
12
13 =head1 SYNOPSIS
14
15     # --keys is the default
16     diff en.yml is.yml
17     diff --keys en.yml is.yml
18
19     # --untranslated-values compares prints keys whose values don't differ
20     diff --untranslated-values-all en.yml is.yml
21
22     # --untranslated-values-all compares prints keys whose values
23     # don't differ. Ignoring the blacklist which prunes things
24     # unlikley to be translated
25     diff --untranslated-values-all en.yml is.yml
26
27 =head1 DESCRIPTION
28
29 This utility prints the differences between two YAML files using
30 L<Test::Differences>. The purpose of it is to diff the files is
31 F<config/locales> to find out what keys need to be added to the
32 translated files when F<en.yml> changes.
33
34 =head1 OPTIONS
35
36 =over
37
38 =item -h, --help
39
40 Print this help message.
41
42 =item --keys
43
44 Show the hash keys that differ between the two files, useful merging
45 new entries from F<en.yml> to a local file.
46
47 =item --untranslated-values
48
49 Show keys whose values are either exactly the same between the two
50 files, or don't exist in the target file (the latter file
51 specified). The values are pruned according to global and language
52 specific blacklists found in the C<__DATA__> section of this script.
53
54 This helps to find untranslated values.
55
56 =item --untranslated-values-all
57
58 Like C<--untranslated-values> but ignores blacklists.
59
60 =back
61
62 =head1 AUTHOR
63
64 E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avar@f-prot.com>
65
66 =cut
67
68 # Get the command-line options
69 Getopt::Long::Parser->new(
70     config => [ qw< bundling no_ignore_case no_require_order pass_through > ],
71 )->getoptions(
72     'h|help' => \my $help,
73     'keys' => \my $keys,
74     'untranslated-values' => \my $untranslated_values,
75     'untranslated-values-all' => \my $untranslated_values_all,
76 ) or help();
77
78 # --keys is the default
79 $keys = 1 if not $untranslated_values_all and not $untranslated_values;
80
81 # On --help
82 help() if $help;
83
84 # If we're not given two .yml files
85 help() if @ARGV != 2 or (!-f $ARGV[0] or !-f $ARGV[1]);
86
87 my ($from, $to) = @ARGV;
88
89 my $from_data = LoadFile($from);
90 my $to_data   = LoadFile($to);
91
92 my $from_parsed = { iterate($from_data->{basename($from)}) };
93 my $to_parsed = { iterate($to_data->{basename($to)}) };
94
95 # Since this used to be the default, support that...
96 if ($keys)
97 {
98     print_key_differences();
99 }
100 elsif ($untranslated_values or $untranslated_values_all)
101 {
102     my @untranslated = untranslated_keys($from_parsed, $to_parsed);
103
104     # Prune according to blacklist
105     if ($untranslated_values) {
106         @untranslated = prune_untranslated_with_blacklist(basename($to), @untranslated);
107     }
108
109     print $_, "\n" for @untranslated;
110 }
111
112 exit 0;
113
114 sub print_key_differences
115 {
116     # Hack around Test::Differences wanting a Test::* module loaded
117     $INC{"Test.pm"} = 1;
118     sub Test::ok { print shift }
119
120     # Diff the tree
121     eq_or_diff([ sort keys %$from_parsed ], [ sort keys %$to_parsed ]);
122 }
123
124 sub untranslated_keys
125 {
126     my ($from_parsed, $to_parsed) = @_;
127     sort grep { not exists $to_parsed->{$_} or $from_parsed->{$_} eq $to_parsed->{$_} } keys %$from_parsed;
128 }
129
130 sub prune_untranslated_with_blacklist
131 {
132     my ($language, @keys) = @_;
133     my %keys;
134     @keys{@keys} = ();
135
136     my $end_yaml = Load(join '', <DATA>);
137     my $untranslated_values = $end_yaml->{untranslated_values};
138     my $default = $untranslated_values->{default};
139     my $this_language = $untranslated_values->{$language} || {};
140
141     my %bw_list = (%$default, %$this_language);
142     
143     while (my ($key, $blacklisted) = each %bw_list)
144     {
145         # FIXME: Does syck actually support true/false booleans in yaml?
146         delete $keys{$key} if $blacklisted eq 'true'
147     }
148
149     sort keys %keys;
150 }
151
152 sub iterate
153 {
154     my ($hash, @path) = @_;
155     my @ret;
156         
157     while (my ($k, $v) = each %$hash)
158     {
159         if (ref $v eq 'HASH')
160         {
161              push @ret => iterate($v, @path, $k);
162         }
163         else
164         {
165             push @ret => join(".",@path, $k), $v;
166         }
167     }
168
169     return @ret;
170 }
171
172 sub basename
173 {
174     my $name = shift;
175     $name =~ s[\..*?$][];
176     $name;
177 }
178
179 sub help
180 {
181     my %arg = @_;
182
183     Pod::Usage::pod2usage(
184         -verbose => $arg{ verbose },
185         -exitval => $arg{ exitval } || 0,
186     );
187 }
188
189 __DATA__
190 untranslated_values:
191
192   # Default/Per language blacklist/whitelist for the
193   # --untranslated-values switch. "true" as a value indicates that the
194   # key is to be blacklisted, and "false" that it's to be
195   # whitelisted. "false" is only required to whitelist a key
196   # blacklisted by default on a per-language basis.
197
198   default:
199     html.dir: true
200     layouts.intro_3_bytemark: true
201     layouts.intro_3_ucl: true
202     layouts.project_name.h1: true
203     layouts.project_name.title: true
204     site.index.license.project_url: true
205   de:
206     activerecord.attributes.message.sender: true
207     activerecord.attributes.trace.name: true
208     activerecord.models.changeset: true
209     activerecord.models.relation: true
210     browse.changeset.changeset: true
211     browse.changeset.changesetxml: true
212     browse.changeset.osmchangexml: true
213     browse.changeset.title: true
214     browse.common_details.version: true
215     browse.containing_relation.relation: true
216     browse.relation.relation: true
217     browse.relation.relation_title: true
218     browse.start_rjs.details: true
219     browse.start_rjs.object_list.details: true
220     browse.tag_details.tags: true
221     changeset.changesets.id: true
222     export.start.export_button: true
223     export.start.format: true
224     export.start.output: true
225     export.start.zoom: true
226     export.start_rjs.export: true
227     layouts.export: true
228     layouts.shop: true
229     layouts.shop_url: true
230     notifier.gpx_notification.failure.import_failures_url: true
231     notifier.signup_confirm_plain.wiki_signup_url: true
232     site.edit.anon_edits: true
233     site.edit.anon_edits_link: true
234     site.index.license.license_name: true
235     site.index.permalink: true
236     site.search.submit_text: true
237     trace.edit.tags: true
238     trace.trace.in: true
239     trace.trace_form.tags: true
240     trace.trace_optionals.tags: true
241     trace.view.tags: true
242     user.account.public editing.enabled link: true
243