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 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 that B<exist in both the compared files> and whose values
54 are exactly the same. Use C<--keys> to a list of values that hasn't
57 The values are pruned according to global and language specific
58 blacklists found in the C<__DATA__> section of this script.
60 This helps to find untranslated values.
62 =item --untranslated-values-all
64 Like C<--untranslated-values> but ignores blacklists.
66 =item --validate-variables
68 Check that interpolated Ruby i18n variables (C<{{foo}}> and
69 C<[[foo]]>) are equivalent in the two provided files.
75 E<AElig>var ArnfjE<ouml>rE<eth> Bjarmason <avar@f-prot.com>
79 # Get the command-line options
80 Getopt::Long::Parser->new(
81 config => [ qw< bundling no_ignore_case no_require_order pass_through > ],
83 'h|help' => \my $help,
85 'untranslated-values' => \my $untranslated_values,
86 'untranslated-values-all' => \my $untranslated_values_all,
87 'validate-variables' => \my $validate_variables,
90 # --keys is the default
91 $keys = 1 if not $untranslated_values_all and not $untranslated_values and not $validate_variables;
96 # If we're not given two .yml files
97 help() if @ARGV != 2 or (!-f $ARGV[0] or !-f $ARGV[1]);
99 my ($from, $to) = @ARGV;
101 my $from_data = LoadFile($from);
102 my $to_data = LoadFile($to);
104 my $from_parsed = { iterate($from_data->{basename($from)}) };
105 my $to_parsed = { iterate($to_data->{basename($to)}) };
109 print_key_differences($from_parsed, $to_parsed);
111 elsif ($untranslated_values or $untranslated_values_all)
113 my @untranslated = untranslated_keys($from_parsed, $to_parsed);
115 # Prune according to blacklist
116 if ($untranslated_values) {
117 @untranslated = prune_untranslated_with_blacklist(basename($to), @untranslated);
120 say for @untranslated;
121 } elsif ($validate_variables)
123 print_validate_variables($from_parsed, $to_parsed);
128 sub print_key_differences
132 # Hack around Test::Differences wanting a Test::* module loaded
134 sub Test::ok { print shift }
137 eq_or_diff([ sort keys %$f ], [ sort keys %$t ]);
140 sub untranslated_keys
142 my ($from_parsed, $to_parsed) = @_;
143 sort grep { exists $to_parsed->{$_} and $from_parsed->{$_} eq $to_parsed->{$_} } keys %$from_parsed;
146 sub prune_untranslated_with_blacklist
148 my ($language, @keys) = @_;
152 my $end_yaml = Load(join '', <DATA>);
153 my $untranslated_values = $end_yaml->{untranslated_values};
154 my $default = $untranslated_values->{default};
155 my $this_language = $untranslated_values->{$language} || {};
157 my %bw_list = (%$default, %$this_language);
159 while (my ($key, $blacklisted) = each %bw_list)
161 # FIXME: Does syck actually support true/false booleans in yaml?
162 delete $keys{$key} if $blacklisted eq 'true'
168 sub print_validate_variables
172 while (my ($key, $val) = each %$f)
174 next if exists $f->{$key} and not exists $t->{$key};
176 my @from_var = parse_variables_from_string($f->{$key});
177 my @to_var = parse_variables_from_string($t->{$key});
179 unless (@from_var ~~ @to_var) {
180 say "$key in $from has (@from_var) and $to has (@to_var)";
186 sub parse_variables_from_string
190 # This probably matches most of the variables
191 my $var = qr/ [a-z0-9_]+? /xs;
193 if (my @var = $string =~ m/ \{\{ ($var) \}\} | \[\[ ($var) \]\] /gsx) {
194 return sort grep { defined } @var;
202 my ($hash, @path) = @_;
205 while (my ($k, $v) = each %$hash)
207 if (ref $v eq 'HASH')
209 push @ret => iterate($v, @path, $k);
213 push @ret => join(".",@path, $k), $v;
223 $name =~ s[\..*?$][];
231 Pod::Usage::pod2usage(
232 -verbose => $arg{ verbose },
233 -exitval => $arg{ exitval } || 0,
240 # Default/Per language blacklist/whitelist for the
241 # --untranslated-values switch. "true" as a value indicates that the
242 # key is to be blacklisted, and "false" that it's to be
243 # whitelisted. "false" is only required to whitelist a key
244 # blacklisted by default on a per-language basis.
248 layouts.intro_3_bytemark: true
249 layouts.intro_3_ucl: true
250 layouts.project_name.h1: true
251 layouts.project_name.title: true
252 site.index.license.project_url: true
253 browse.relation_member.entry: true
256 activerecord.attributes.message.sender: true
257 activerecord.attributes.trace.name: true
258 activerecord.models.changeset: true
259 activerecord.models.relation: true
260 browse.changeset.changeset: true
261 browse.changeset.changesetxml: true
262 browse.changeset.osmchangexml: true
263 browse.changeset.title: true
264 browse.common_details.version: true
265 browse.containing_relation.relation: true
266 browse.relation.relation: true
267 browse.relation.relation_title: true
268 browse.start_rjs.details: true
269 browse.start_rjs.object_list.details: true
270 browse.tag_details.tags: true
271 changeset.changesets.id: true
272 export.start.export_button: true
273 export.start.format: true
274 export.start.output: true
275 export.start.zoom: true
276 export.start_rjs.export: true
279 site.edit.anon_edits: true
280 site.index.license.license_name: true
281 site.index.permalink: true
282 site.key.table.entry.park: true
283 site.search.submit_text: true
284 trace.edit.tags: true
286 trace.trace_form.tags: true
287 trace.trace_optionals.tags: true
288 trace.view.tags: true
289 user.account.public editing.enabled link: true
293 site.edit.anon_edits: true
295 # Creative Commons Attribution-Share Alike 2.0
296 site.index.license.license_name: true
298 # http://creativecommons.org/licenses/by-sa/2.0/
299 site.index.license.license_url: true