9 POSIX
::setlocale
(POSIX
::LC_ALL
, 'C');
11 @ARGV >= 1 || die "Usage: $0 <source directory>\n";
15 '.js' => [ '_:1', '_:1,2c', 'N_:2,3', 'N_:2,3,4c' ],
16 '.lua' => [ '_:1', '_:1,2c', 'translate:1', 'translate:1,2c', 'translatef:1', 'N_:2,3', 'N_:2,3,4c', 'ntranslate:2,3', 'ntranslate:2,3,4c' ],
17 '.htm' => [ '_:1', '_:1,2c', 'translate:1', 'translate:1,2c', 'translatef:1', 'N_:2,3', 'N_:2,3,4c', 'ntranslate:2,3', 'ntranslate:2,3,4c' ],
18 '.json' => [ '_:1', '_:1,2c' ]
24 my ($ext) = $path =~ m!(\.\w+)$!;
25 my @cmd = qw(xgettext --from-code=UTF-8 --no-wrap);
27 if ($ext eq '.htm' || $ext eq '.lua') {
28 push @cmd, '--language=Lua';
30 elsif ($ext eq '.js' || $ext eq '.json') {
31 push @cmd, '--language=JavaScript';
34 push @cmd, map { "--keyword=$_" } (@
{$keywords{$ext}}, @keywords);
40 sub whitespace_collapse
($) {
42 my %r = ('n' => ' ', 't' => ' ');
44 # Translate \t and \n to plain spaces, leave all other escape
45 # sequences alone. Finally replace all consecutive spaces by
46 # single ones and trim leading and trailing space.
47 $s =~ s/\\(.)/$r{$1} || "\\$1"/eg;
55 sub postprocess_pot
($$) {
56 my ($path, $source) = @_;
60 $source =~ s/^#: (.+?)\n/join("\n", map { "#: $path:$_" } $1 =~ m!:(\d+)!g) . "\n"/emg;
62 my @lines = split /\n/, $source;
64 # Remove all header lines up to the first location comment
65 while (@lines > 0 && $lines[0] !~ m!^#: !) {
70 my $line = shift @lines;
72 # Concat multiline msgids and collapse whitespaces
73 if ($line =~ m!^(msg\w+) "(.*)"$!) {
77 while (@lines > 0 && $lines[0] =~ m!^"(.*)"$!) {
82 $kv = whitespace_collapse
($kv);
84 # Filter invalid empty msgids by popping all lines in @res
85 # leading to this point and skip all subsequent lines in
86 # @lines belonging to this faulty id.
87 if ($kw ne 'msgstr' && $kv eq '') {
88 while (@res > 0 && $res[-1] !~ m!^$!) {
92 while (@lines > 0 && $lines[0] =~ m!^(?:msg\w+ )?"(.*)"$!) {
99 push @res, sprintf '%s "%s"', $kw, $kv;
102 # Ignore any flags added by xgettext
103 elsif ($line =~ m!^#, !) {
107 # Pass through other lines unmodified
113 return @res ?
join("\n", '', @res, '') : '';
117 my %h = map { $_, 1 } @_;
121 sub preprocess_htm
($$) {
122 my ($path, $source) = @_;
125 '_' => 'translate([==[%s]==])',
126 ':' => 'translate([==[%s]==])',
127 '+' => 'include([==[%s]==])',
128 '#' => '--[==[%s]==]',
132 # Translate the .htm source into a valid Lua source using bracket quotes
133 # to avoid the need for complex escaping.
134 $source =~ s
!<%-?
([=_
:+#]?)(.*?)-?%>!
138 # Split translation expressions on first non-escaped pipe.
139 if ($t eq ':' || $t eq '_') {
140 $s =~ s/^((?:[^\|\\]|\\.)*)\|(.*)$/$1]==],[==[$2/;
143 sprintf "]==]; $sub->{$t}; [==[", $s
146 # Discover expressions like "lng.translate(...)" or "luci.i18n.translate(...)"
147 # and return them as extra keyword so that xgettext recognizes such expressions
148 # as translate(...) calls.
149 my @extra_function_keywords =
150 map { ("$_:1", "$_:1,2c") }
151 uniq
($source =~ m!((?:\w+\.)+translatef?)[ \t\n]*\(!g);
153 return ("[==[$source]==]", @extra_function_keywords);
156 sub preprocess_lua
($$) {
157 my ($path, $source) = @_;
159 # Discover expressions like "lng.translate(...)" or "luci.i18n.translate(...)"
160 # and return them as extra keyword so that xgettext recognizes such expressions
161 # as translate(...) calls.
162 my @extra_function_keywords =
163 map { ("$_:1", "$_:1,2c") }
164 uniq
($source =~ m!((?:\w+\.)+translatef?)[ \t\n]*\(!g);
166 return ($source, @extra_function_keywords);
169 sub preprocess_json
($$) {
170 my ($path, $source) = @_;
171 my ($file) = $path =~ m!([^/]+)$!;
173 $source =~ s/("(?:title|description)")\s*:\s*("(?:[^"\\]|\\.)*")/$1: _($2)/sg;
179 my ($msguniq_in, $msguniq_out);
180 my $msguniq_pid = open2
($msguniq_out, $msguniq_in, 'msguniq', '-s');
182 print $msguniq_in "msgid \"\"\nmsgstr \"Content-Type: text/plain; charset=UTF-8\"\n";
184 if (open F
, "find @ARGV -type f '(' -name '*.htm' -o -name '*.lua' -o -name '*.js' -o -path '*/menu.d/*.json' -o -path '*/acl.d/*.json' -o -path '*/statistics/plugins/*.json' ')' |")
186 while (defined( my $file = readline F
))
190 if (open S
, '<', $file)
194 my @extra_function_keywords;
196 if ($file =~ m!\.htm$!)
198 ($source, @extra_function_keywords) = preprocess_htm
($file, $source);
200 elsif ($file =~ m!\.lua$!)
202 ($source, @extra_function_keywords) = preprocess_lua
($file, $source);
204 elsif ($file =~ m!\.json$!)
206 ($source, @extra_function_keywords) = preprocess_json
($file, $source);
209 my ($xgettext_in, $xgettext_out);
210 my $pid = open2
($xgettext_out, $xgettext_in, xgettext
($file, @extra_function_keywords), '-');
212 print $xgettext_in $source;
215 my $pot = readline $xgettext_out;
220 print $msguniq_in postprocess_pot
($file, $pot);
229 my @pot = <$msguniq_out>;
232 waitpid $msguniq_pid, 0;
235 my $line = shift @pot;
237 # Reorder the location comments in a detemrinistic way to
238 # reduce SCM noise when frequently updating templates.
239 if ($line =~ m!^#: !) {
242 while (@pot > 0 && $pot[0] =~ m!^#: !) {
243 push @locs, shift @pot;
247 map { join(':', @
$_) . "\n" }
248 sort { ($a->[0] cmp $b->[0]) || ($a->[1] <=> $b->[1]) }
249 map { [ /^(.+):(\d+)$/ ] }