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