A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1#!/usr/bin/perl -s
2# __________ __ ___.
3# Open \______ \ ____ ____ | | _\_ |__ _______ ___
4# Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5# Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6# Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7# \/ \/ \/ \/ \/
8# $Id$
9#
10# Copyright (C) 2006 - 2008 by Daniel Stenberg
11#
12
13# See apps/language.c (TODO: Use common include for both)
14# Cookie and binary version for the binary lang file
15my $LANGUAGE_COOKIE = 0x1a;
16my $VOICE_COOKIE = 0x9a;
17my $LANGUAGE_VERSION = 0x06;
18my $LANGUAGE_FLAG_RTL = 0x01;
19my $LANGUAGE_FLAG_UNITS_FIRST = 0x02;
20
21my $HEADER_SIZE = 4;
22my $SUBHEADER_SIZE = 6;
23
24if(!$ARGV[0]) {
25 print <<MOO
26Usage: genlang [options] <langv2 file>
27
28 -p=<prefix>
29 Make the tool create a [prefix].c and [prefix].h file.
30
31 -b=<outfile>
32 Make the tool create a binary language (.lng) file named [outfile].
33 The use of this option requires that you also use -e, -t and -i.
34
35 -c=<outfile>
36 Create binary voicestring file named [outfile]. Works like -b and can be
37 used the same time.
38
39 -e=<english lang file>
40 Point out the english (original source) file, to use that as master
41 language template. Always required.
42
43 -t=<target>
44 Specify which target you want the translations/phrases for. Required when
45 -b, -o, or -p is used.
46
47 The target can in fact be specified as numerous different strings,
48 separated with colons. This will make genlang to use all the specified
49 strings when searching for a matching phrase.
50
51 -i=<target id>
52 The target id number, needed for -b.
53
54 -o
55 Voice mode output. Outputs all id: and voice: lines for the given target!
56
57 -v
58 Enables verbose (debug) output.
59MOO
60;
61 exit;
62}
63
64my $prefix = $p;
65my $binary = $b;
66my $binvoice = $c;
67my $voiceout = $o;
68
69my $english = $e;
70if (!$english) {
71 print STDERR "Please specify the english lang source (with -e)!\n";
72 exit;
73}
74
75my $target_id = $i;
76if($binary && !$target_id) {
77 print STDERR "Please specify a target id number (with -i)!\n";
78 exit;
79}
80
81my $target = $t;
82if(!$target) {
83 print STDERR "Please specify a target (with -t)!\n";
84 exit;
85}
86
87my $check = ($binary?.5:0) + ($prefix?1:0) + ($voiceout?1:0) + ($binvoice?.5:0);
88if($check > 1) {
89 print STDERR "Please use only one of -p, -o, -b, and -c\n";
90 exit;
91}
92if(!$check) {
93 print STDERR "Please use at least one of -p, -o, -c, and -e\n";
94 exit;
95}
96
97# Build up a regex which can be applied to target wildcard lists. We only need
98# to support prefix matches, so a target parameter of foo:bar can be expanded
99# to the regex "\*|f\*|fo\*|foo|b\*|ba\*|bar" and applied to the wildcard list
100# (plus end-of-string or commas on either side). The regex engine should
101# discard any duplicates generated for us in the process of constructing the
102# state machine, so we don't bother to check.
103my $target_regex = "(?:^|,) *(?:\\*";
104my $model_regex = ""; # This matches the player model only!
105foreach my $target_part (split ':', $target) {
106 for (my $c=1; $c<=length $target_part; $c++) {
107 my $partial = substr $target_part, 0, $c;
108 $target_regex .= "|$partial\\*";
109 }
110 $target_regex .= "|$target_part";
111 $model_regex = $target_regex if (!$model_regex);
112}
113$target_regex .= ") *(?:,|\$)";
114$target_regex = qr/$target_regex/;
115$model_regex .= ") *(?:,|\$)";
116$model_regex = qr/$model_regex/;
117
118my $binpath = "";
119if ($binary =~ m|(.*)/[^/]+|) {
120 $binpath = $1;
121}
122
123my $verbose=$v;
124
125my %id; # string to num hash
126my @idnum; # num to string array
127
128my %allphrases; # For sorting - an array of the <phrase> elements
129my %source; # id string to source phrase hash
130my %dest; # id string to dest phrase hash
131my %voice; # id string to voice phrase hash
132
133my %users =
134 ('core' => 0);
135
136my $input = $ARGV[0];
137
138my @m;
139my $m="blank";
140
141sub trim {
142 my ($string) = @_;
143 $string =~ s/^\s+//;
144 $string =~ s/\s+$//;
145 return $string;
146}
147
148sub blank {
149 # nothing to do
150}
151
152my %head;
153sub header {
154 my ($full, $n, $v)=@_;
155 $head{$n}=$v;
156}
157
158my %phrase;
159sub phrase {
160 my ($full, $n, $v)=@_;
161 $phrase{$n}=$v;
162}
163
164my %options;
165sub options {
166 my ($full, $n, $v)=@_;
167 $options{$n}=$v;
168}
169
170sub parsetarget {
171 my ($debug, $strref, $full, $n, $v)=@_;
172 my $string;
173 if ($n =~ $target_regex) {
174 $string = $v;
175 # Only override the previously set string if this is a device-specific match
176 $$strref = $string if (!$$strref || $$strref eq 'none' || $n =~ $model_regex);
177 return $string;
178 }
179}
180
181my $src;
182sub source {
183 parsetarget("src", \$src, @_);
184}
185
186my $dest;
187sub dest {
188 parsetarget("dest", \$dest, @_);
189}
190
191my $voice;
192sub voice {
193 parsetarget("voice", \$voice, @_);
194}
195
196sub file_is_newer {
197 my ($file1, $file2) = @_;
198
199 my @s1 = stat $file1;
200 my @s2 = stat $file2;
201
202 return 1 if ($s1[9] > $s2[9]);
203 return 0;
204}
205
206my %idmap;
207my %english;
208if($english) {
209 readenglish();
210}
211
212sub readenglish {
213 # For the cases where the english file needs to be scanned/read, we must
214 # do it before we read the translated file.
215
216 my @idnum = ((0)); # start with a true number
217 my @vidnum = ((0x8000)); # first voice id
218
219 if ($binary and file_is_newer("$binpath/english.list", $english)) {
220 open(ENG, "<$binpath/english.list") ||
221 die "Error: can't open $binpath/english.list";
222 while (<ENG>) {
223 my ($user, $id, $value) = split ':', $_;
224 $idmap[$user]{$id} = $value;
225 $english{$id} = 1;
226 }
227 close ENG;
228
229 return;
230 }
231
232 open(ENG, "<$english") || die "Error: can't open $english";
233 my @phrase;
234 my $id;
235 my $maybeid;
236 my $user;
237 my $withindest;
238 my $numphrases = 0;
239 my $numusers = 1; # core is already in the users map
240
241 while(<ENG>) {
242 # get rid of DOS newlines
243 $_ =~ tr/\r//d;
244
245 if($_ =~ /^ *\<phrase\>/) {
246 # this is the start of a phrase
247 } elsif($_ =~ /\<\/phrase\>/) {
248 # if id is something, when we count and store this phrase
249 if($id) {
250 # voice-only entries get a difference range
251 if($id =~ /^VOICE_/) {
252 # Assign an ID number to this entry
253 $idmap[$user]{$id}=$vidnum[$user];
254 $vidnum[$user]++;
255 } else {
256 # Assign an ID number to this entry
257 $idmap[$user]{$id}=$idnum[$user];
258 $idnum[$user]++;
259 # print STDERR "DEST: bumped idnum to $idnum[$user]\n";
260 }
261
262 # this is the end of a phrase, add it to the english hash
263 $english{$id}=join("", @phrase);
264 }
265 undef @phrase;
266 $id="";
267 } elsif($_ ne "\n") {
268 # gather everything related to this phrase
269 push @phrase, $_;
270 if($_ =~ /^ *\<dest\>/i) {
271 $withindest=1;
272 $deststr="";
273 } elsif($withindest && ($_ =~ /^ *\<\/dest\>/i)) {
274 $withindest=0;
275 if($deststr && ($deststr !~ /^none\z/i)) {
276 # we unconditionally always use all IDs when the "update"
277 # feature is used
278 $id = $maybeid;
279 # print "DEST: use this id $id\n";
280 } else {
281 # print "skip $maybeid for $name\n";
282 }
283 } elsif($withindest && ($_ =~ / *([^:]+): *(.*)/)) {
284 my ($name, $val)=($1, $2);
285 $dest=""; # in case it is left untouched for when the
286 # model name isn't "ours"
287 dest($_, $name, $val);
288
289 if($dest) {
290 # Store the current dest string. If this target matches
291 # multiple strings, it will get updated several times.
292 $deststr = $dest;
293 }
294 }
295 }
296
297 if($_ =~ /^ *id: ([^ \t\n]+)/i) {
298 $maybeid=$1;
299 $sortorder{$maybeid}=$numphrases++;
300 }
301 if($_ =~ /^ *user: ([^ \t\n]+)/i) {
302 $user = $users{$1};
303 if(!(defined $user)) {
304 $user = ++$numusers;
305 $users{$1} = $user;
306 }
307 }
308 }
309 close(ENG);
310}
311
312my @idcount; # counter for lang ID numbers
313my @voiceid; # counter for voice-only ID numbers
314
315for (keys %users) {
316 push @idcount, 0;
317 push @voiceid, 0x8001;
318}
319
320#
321# Now start the scanning of the selected language string
322#
323
324open(LANG, "<$input") || die "Error: couldn't read language file named $input\n";
325my @phrase;
326my $langoptions = 0;
327
328while(<LANG>) {
329
330 $line++;
331
332 # get rid of DOS newlines
333 $_ =~ tr/\r//d;
334
335 if($_ =~ /^( *\#|[ \t\n\r]*\z)/) {
336 # comment or empty line - output it if it's part of the header
337 if ($_ =~ /LANGUAGE_IS_RTL/) {
338 $langoptions |= $LANGUAGE_FLAG_RTL;
339 }
340 if ($_ =~ /LANGUAGE_UNITS_FIRST/) {
341 $langoptions |= $LANGUAGE_FLAG_UNITS_FIRST;
342 }
343 next;
344 }
345
346 my $ll = $_;
347
348 # print "M: $m\n";
349
350 push @phrase, $ll;
351
352 # this is an XML-lookalike tag
353 if (/^(<|[^\"<]+<)([^>]*)>/) {
354 my $part = $2;
355 # print "P: $part\n";
356
357 if($part =~ /^\//) {
358 # this was a closing tag
359
360 if($part eq "/phrase") {
361 # closing the phrase
362
363 my $idstr = $phrase{'id'};
364 my $idnum;
365
366 if($english && !$english{$idstr}) {
367 # print STDERR "$idstr doesn't exist for english, skip it\n";
368 } elsif($dest =~ /^none\z/i || $src =~ /^none\z/i ) {
369 # "none" as dest (without quotes) means that this entire
370 # phrase is to be ignored
371 } elsif($sortfile) {
372 $allphrases{$idstr}=join('',@phrase);
373 } else {
374 # allow the keyword 'deprecated' to be used on dest and
375 # voice strings to mark that as deprecated. It will then
376 # be replaced with "".
377
378 $dest =~ s/^deprecate(|d)\z/\"\"/i;
379 $voice =~ s/^deprecate(|d)\z/\"\"/i;
380
381 # basic syntax error alerts, if there are no quotes we
382 # will assume an empty string was intended
383 if($dest !~ /^\"/) {
384 print STDERR "$input:$line:1: warning: dest before line lacks quotes ($dest)!\n";
385 $dest='""';
386 }
387 if($src !~ /^\"/) {
388 print STDERR "$input:$line:1: warning: source before line lacks quotes ($src)!\n";
389 $src='""';
390 }
391 if($voice !~ /^\"/ and $voice !~ /^none\z/i) {
392 print STDERR "$input:$line:1: warning: voice before line lacks quotes ($voice)!\n";
393 $voice='""';
394 }
395 if($dest eq '""' && $phrase{'desc'} !~ /deprecated/i && $idstr !~ /^VOICE/) {
396 print STDERR "$input:$line:1: warning: empty dest before line in non-deprecated phrase!\n";
397 }
398
399 my $userstr = trim($phrase{'user'});
400 my $user = $users{$userstr};
401 if ($userstr eq "") {
402 print STDERR "$input:$line:1: warning: missing user!\n";
403 $user = $users{"core"};
404 }
405 elsif(!(defined $user)) {
406 if($english) {
407 print STDERR "$input:$line:1: warning: user was not found in $english!\n";
408 $user = keys %users; # set to an invalid user so it won't be added
409 } else {
410 # we found a new user, add it to the usermap
411 $user = ++$numusers;
412 $users{$userstr} = $user;
413 }
414 }
415
416 # Use the ID name to figure out which id number range we
417 # should use for this phrase. Voice-only strings are
418 # separated.
419
420 if($idstr =~ /^VOICE/) {
421 $idnum = $voiceid[$user]++;
422 } else {
423 $idnum = $idcount[$user]++;
424 }
425
426 # Strip out the magic "Same as english" flag
427 $dest =~ s/^("?)~+/$1/;
428 $voice =~ s/^("?)~+/$1/;
429
430 $id{$idstr} = $idnum;
431 $idnum[$user][$idnum]=$idstr;
432
433 $source{$idstr}=$src;
434 $dest{$idstr}=$dest;
435 $voice{$idstr}=$voice;
436
437 if($verbose) {
438 print "id: $phrase{id} ($idnum)\n";
439 print "source: $src\n";
440 print "dest: $dest\n";
441 print "voice: $voice\n";
442 print "user: $user\n";
443 }
444
445 undef $src;
446 undef $dest;
447 undef $voice;
448 undef $user;
449 undef %phrase;
450 }
451 undef @phrase;
452 } # end of </phrase>
453
454 # starts with a slash, this _ends_ this section
455 $m = pop @m; # get back old value, the previous level's tag
456 next;
457 } # end of tag close
458
459 # This is an opening (sub) tag
460
461 push @m, $m; # store old value
462 $m = $part;
463 next;
464 }
465
466 if(/^ *([^:]+): *(.*)/) {
467 my ($name, $val)=($1, $2);
468 &$m($_, $name, $val);
469 }
470}
471close(LANG);
472
473if ($sortfile) {
474 for(sort { $sortorder{$a} <=> $sortorder{$b} } keys %allphrases) {
475 print $allphrases{$_};
476 }
477}
478
479if($prefix) {
480 # We create a .c and .h file
481
482 open(HFILE_CORE, ">$prefix/lang.h") ||
483 die "Error: couldn't create file $prefix/lang.h\n";
484 open(CFILE_CORE, ">$prefix/lang_core.c") ||
485 die "Error: couldn't create file $prefix/lang_core.c\n";
486
487 # get header file name
488 $headername = "$prefix/lang.h";
489 $headername =~ s/(.*\/)*//;
490
491 print HFILE_CORE <<MOO
492/* This file was automatically generated using genlang */
493/*
494 * The str() macro/functions is how to access strings that might be
495 * translated. Use it like str(MACRO) and expect a string to be
496 * returned!
497 */
498#define str(x) language_strings[x]
499
500/* this is the array for holding the string pointers.
501 It will be initialized at runtime. */
502extern unsigned char *language_strings[];
503/* this contains the concatenation of all strings, separated by \\0 chars */
504extern const unsigned char core_language_builtin[];
505
506#include "${prefix}_enum.h"
507
508MOO
509 ;
510
511 close(HFILE_CORE);
512
513 open(HFILE_CORE, ">${prefix}_enum.h") ||
514 die "couldn't create file ${prefix}_enum.h\n";
515
516 print HFILE_CORE <<MOO
517/* This file was automatically generated using genlang */
518#ifndef _LANG_ENUM_H_
519#define _LANG_ENUM_H_
520/* The enum below contains all available strings */
521enum \{
522MOO
523 ;
524
525 print CFILE_CORE <<MOO
526/* This file was automatically generated using genlang, the strings come
527 from "$input" */
528
529#include "$headername"
530
531unsigned char *language_strings[LANG_LAST_INDEX_IN_ARRAY];
532const unsigned char core_language_builtin[] =
533MOO
534;
535
536 # Output the ID names for the enum in the header file
537 my $i;
538 for $i (0 .. $idcount[$users{"core"}]-1) {
539 my $name=$idnum[$users{"core"}][$i]; # get the ID name
540
541 $name =~ tr/\"//d; # cut off the quotes
542
543 printf HFILE_CORE (" %s, /* %d */\n", $name, $i);
544 }
545
546# Output separation marker for last string ID and the upcoming voice IDs
547
548 print HFILE_CORE <<MOO
549 LANG_LAST_INDEX_IN_ARRAY, /* this is not a string, this is a marker */
550 /* --- below this follows voice-only strings --- */
551 VOICEONLY_DELIMITER = 0x8000,
552MOO
553 ;
554
555# Output the ID names for the enum in the header file
556 for $i (0x8001 .. ($voiceid[$users{"core"}]-1)) {
557 my $name=$idnum[$users{"core"}][$i]; # get the ID name
558
559 $name =~ tr/\"//d; # cut off the quotes
560
561 printf HFILE_CORE (" %s, /* 0x%x */\n", $name, $i);
562 }
563
564 # Output end of lang_enum.h
565 print HFILE_CORE <<MOO
566 LANG_LAST_VOICEONLY_INDEX_IN_ARRAY /* this is not a string, this is a marker */
567};
568/* end of generated enum list */
569#endif /* _LANG_ENUM_H_ */
570MOO
571 ;
572
573 # Output the target phrases for the source file
574 for $i (0 .. $idcount[$users{"core"}]-1) {
575 my $name=$idnum[$users{"core"}][$i]; # get the ID
576 my $dest = $dest{$name}; # get the destination phrase
577
578 $dest =~ s:\"$:\\0\":; # insert a \0 before the second quote
579
580 if(!$dest) {
581 # this is just to be on the safe side
582 $dest = '"\0"';
583 }
584
585 printf CFILE_CORE (" %s\n", $dest);
586 }
587
588# Output end of string chunk
589 print CFILE_CORE <<MOO
590;
591/* end of generated string list */
592MOO
593;
594
595 close(HFILE_CORE);
596 close(CFILE_CORE);
597} # end of the c/h file generation
598elsif($binary || $binvoice) {
599 # Creation of a binary lang file was requested
600
601 # We must first scan the english file to get the correct order of the id
602 # numbers used there, as that is what sets the id order for all language
603 # files. The english file is scanned before the translated file was
604 # scanned.
605
606 if($binary) {
607 open(OUTF, ">$binary") or die "Error: Can't create $binary";
608 binmode OUTF;
609 printf OUTF ("%c%c%c%c", $LANGUAGE_COOKIE, $LANGUAGE_VERSION, $target_id,
610 $langoptions); # magic lang file header
611 }
612 if($binvoice) {
613 open(OUTV, ">$binvoice") or die "Error: Can't create $binary";
614 binmode OUTV;
615 printf OUTV ("%c%c%c%c", $VOICE_COOKIE, $LANGUAGE_VERSION, $target_id,
616 $langoptions); # magic lang file header
617 }
618
619 # output the number of strings for each user
620 my $foffset = $HEADER_SIZE + $SUBHEADER_SIZE * keys(%users);
621 for (keys %users) {
622 my $size;
623 for $n (0 .. $idcount[$_]-1) {
624 $size += length(trim($dest{$idnum[$_][$n]})) + 1;
625 }
626 if($binary) {
627 printf OUTF ("%c%c%c%c%c%c", ($idcount[$_] >> 8), ($idcount[$_] & 0xff),
628 ($size >> 8), ($size & 0xff), ($foffset >> 8), ($foffset & 0xff));
629 }
630 if($binvoice) {
631 printf OUTV ("%c%c%c%c%c%c", ($idcount[$_] >> 8), ($idcount[$_] & 0xff),
632 ($size >> 8), ($size & 0xff), ($foffset >> 8), ($foffset & 0xff));
633 }
634 $foffset += $size;
635 }
636
637 for (keys %users) {
638 # loop over the target phrases
639 # This loops over the strings in the translated language file order
640 my @ids = ((0 .. ($idcount[$_]-1)));
641 push @ids, (0x8001 .. ($voiceid[$_]-1));
642 for $n (@ids) {
643 my $name=$idnum[$_][$n]; # get the ID
644 my $dest = $dest{$name}; # get the destination phrase
645 my $voice = $voice{$name}; # get the destination voice string
646
647 if($dest && $n < 0x8000 && $binary) {
648 $dest =~ s/^\"(.*)\"\s*$/$1/g; # cut off quotes
649
650 # Now, make sure we get the number from the english sort order:
651 $idnum = $idmap[$_]{$name};
652
653 printf OUTF ("%c%c%s\x00", ($idnum>>8), ($idnum&0xff), $dest);
654 }
655 if($voice && $binvoice) {
656 $voice =~ s/^\"(.*)\"\s*$/$1/g; # cut off quotes
657 # Now, make sure we get the number from the english sort order:
658 $idnum = $idmap[$_]{$name};
659 printf OUTV ("%c%c%s\x00", ($idnum>>8), ($idnum&0xff), $voice);
660 }
661 }
662 }
663 if($binary) {
664 close(OUTF);
665 }
666 if($binvoice) {
667 close(OUTV);
668 }
669}
670elsif($voiceout) {
671 # voice output requested, display id: and voice: strings in a v1-like
672 # fashion
673
674 my @engl;
675
676 for (keys %users) {
677 # loop over the target phrases
678 # This loops over the strings in the translated language file order
679 my @ids = ((0 .. ($idcount[$_]-1)));
680 push @ids, (0x8001 .. ($voiceid[$_]-1));
681 for $n (@ids) {
682 my $name=$idnum[$_][$n]; # get the ID
683 my $dest = $dest{$name}; # get the destination phrase
684 my $voice = $voice{$name}; # get the destination voice string
685
686 if($voice) {
687 $voice =~ s/^\"(.*)\"\s*$/$1/g; # cut off quotes
688 # Now, make sure we get the number from the english sort order:
689 $idnum = $idmap[$_]{$name};
690 $engl[$idnum] = "#$idnum ($n)\nid: $name\nvoice: \"$voice\"\n";
691 }
692 }
693 }
694 # Print the list in the the English sort order
695 for (@engl) {
696 print $_;
697 }
698}
699
700
701if($verbose) {
702 my $num_str = 0;
703
704 for (keys %users) {
705 $num_str += $idcount[$_];
706 }
707
708 printf("%d ID strings scanned\n", $num_str);
709
710 print "* head *\n";
711 for(keys %head) {
712 printf "$_: %s\n", $head{$_};
713 }
714}
715
716if ($binary and !file_is_newer("$binpath/english.list", $english)) {
717 open(ENGLIST, ">$binpath/english.list") ||
718 die "Failed creating $binpath/english.list";
719 for my $user (keys %users) {
720 for my $id (sort { $idmap[$user]{$a} <=> $idmap[$user]{$b} } keys %{$idmap[$user]}) {
721 print ENGLIST "$user:$id:$idmap[$user]{$id}\n";
722 }
723 }
724 close ENGLIST;
725}