| #!/usr/bin/perl |
| |
| # |
| #//===----------------------------------------------------------------------===// |
| #// |
| #// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| #// See https://llvm.org/LICENSE.txt for license information. |
| #// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| #// |
| #//===----------------------------------------------------------------------===// |
| # |
| |
| use strict; |
| use warnings; |
| |
| use File::Glob ":glob"; |
| use Encode qw{ encode }; |
| |
| use FindBin; |
| use lib "$FindBin::Bin/lib"; |
| |
| use tools; |
| |
| our $VERSION = "0.04"; |
| my $escape = qr{%}; |
| my $placeholder = qr{(\d)\$(s|l?[du])}; |
| my $target_os; |
| |
| my $sections = |
| { |
| meta => { short => "prp" }, # "prp" stands for "property". |
| strings => { short => "str" }, |
| formats => { short => "fmt" }, |
| messages => { short => "msg" }, |
| hints => { short => "hnt" }, |
| }; |
| my @sections = qw{ meta strings formats messages hints }; |
| # Assign section properties: long name, set number, base number. |
| map( $sections->{ $sections[ $_ ] }->{ long } = $sections[ $_ ], ( 0 .. @sections - 1 ) ); |
| map( $sections->{ $sections[ $_ ] }->{ set } = ( $_ + 1 ), ( 0 .. @sections - 1 ) ); |
| map( $sections->{ $sections[ $_ ] }->{ base } = ( ( $_ + 1 ) << 16 ), ( 0 .. @sections - 1 ) ); |
| |
| # Properties of Meta section. |
| my @properties = qw{ Language Country LangId Version Revision }; |
| |
| |
| sub _generate_comment($$$) { |
| |
| my ( $data, $open, $close ) = @_; |
| my $bulk = |
| $open . " Do not edit this file! " . $close . "\n" . |
| $open . " The file was generated from " . get_file( $data->{ "%meta" }->{ source } ) . |
| " by " . $tool . " on " . localtime() . ". " . $close . "\n"; |
| return $bulk; |
| |
| }; # sub _generate_comment |
| |
| |
| sub msg2sgn($) { |
| |
| # Convert message string to signature. Signature is a list of placeholders in sorted order. |
| # For example, signature of "%1$s value \"%2$s\" is invalid." is "%1$s %2$s". |
| |
| my ( $msg ) = @_; |
| my @placeholders; |
| pos( $msg ) = 0; |
| while ( $msg =~ m{\G.*?$escape$placeholder}g ) { |
| $placeholders[ $1 - 1 ] = "%$1\$$2"; |
| }; # while |
| for ( my $i = 1; $i <= @placeholders; ++ $i ) { |
| if ( not defined( $placeholders[ $i - 1 ] ) ) { |
| $placeholders[ $i - 1 ] = "%$i\$-"; |
| }; # if |
| }; # for $i |
| return join( " ", @placeholders ); |
| |
| }; # sub msg2sgn |
| |
| |
| sub msg2src($) { |
| |
| # Convert message string to a C string constant. |
| |
| my ( $msg ) = @_; |
| if ( $target_os eq "win" ) { |
| $msg =~ s{$escape$placeholder}{\%$1!$2!}g; |
| }; # if |
| return $msg; |
| |
| }; # sub msg2src |
| |
| |
| my $special = |
| { |
| "n" => "\n", |
| "t" => "\t", |
| }; |
| |
| sub msg2mc($) { |
| my ( $msg ) = @_; |
| $msg = msg2src( $msg ); # Get windows style placeholders. |
| $msg =~ s{\\(.)}{ exists( $special->{ $1 } ) ? $special->{ $1 } : $1 }ge; |
| return $msg; |
| }; # sub msg2mc |
| |
| |
| |
| sub parse_message($) { |
| |
| my ( $msg ) = @_; |
| pos( $msg ) = 0; |
| for ( ; ; ) { |
| if ( $msg !~ m{\G.*?$escape}gc ) { |
| last; |
| } |
| if ( $msg !~ m{\G$placeholder}gc ) { |
| return "Bad %-sequence near \"%" . substr( $msg, pos( $msg ), 7 ) . "\""; |
| }; # if |
| }; # forever |
| return undef; |
| |
| }; # sub parse_message |
| |
| |
| sub parse_source($) { |
| |
| my ( $name ) = @_; |
| |
| my @bulk = read_file( $name, -layer => ":utf8" ); |
| my $data = {}; |
| |
| my $line; |
| my $n = 0; # Line number. |
| my $obsolete = 0; # Counter of obsolete entries. |
| my $last_idx; |
| my %idents; |
| my $section; |
| |
| my $error = |
| sub { |
| my ( $n, $line, $msg ) = @_; |
| runtime_error( "Error parsing $name line $n: " . "$msg:\n" . " $line" ); |
| }; # sub |
| |
| foreach $line ( @bulk ) { |
| ++ $n; |
| # Skip empty lines and comments. |
| if ( $line =~ m{\A\s*(\n|#)} ) { |
| $last_idx = undef; |
| next; |
| }; # if |
| # Parse section header. |
| if ( $line =~ m{\A-\*-\s*([A-Z_]*)\s*-\*-\s*\n\z}i ) { |
| $section = ( lc( $1 ) ); |
| if ( not grep( $section eq $_, @sections ) ) { |
| $error->( $n, $line, "Unknown section \"$section\" specified" ); |
| }; # if |
| if ( exists( $data->{ $section } ) ) { |
| $error->( $n, $line, "Multiple sections of the same type specified" ); |
| }; # if |
| %idents = (); # Clean list of known message identifiers. |
| next; |
| }; # if |
| if ( not defined( $section ) ) { |
| $error->( $n, $line, "Section heading expected" ); |
| }; # if |
| # Parse section body. |
| if ( $section eq "meta" ) { |
| if ( $line =~ m{\A([A-Z_][A-Z_0-9]*)\s+"(.*)"\s*?\n?\z}i ) { |
| # Parse meta properties (such as Language, Country, and LangId). |
| my ( $property, $value ) = ( $1, $2 ); |
| if ( not grep( $_ eq $property , @properties ) ) { |
| $error->( $n, $line, "Unknown property \"$property\" specified" ); |
| }; # if |
| if ( exists( $data->{ "%meta" }->{ $property } ) ) { |
| $error->( $n, $line, "Property \"$property\" has already been specified" ); |
| }; # if |
| $data->{ "%meta" }->{ $property } = $value; |
| $last_idx = undef; |
| next; |
| }; # if |
| $error->( $n, $line, "Property line expected" ); |
| }; # if |
| # Parse message. |
| if ( $line =~ m{\A([A-Z_][A-Z_0-9]*)\s+"(.*)"\s*?\n?\z}i ) { |
| my ( $ident, $message ) = ( $1, $2 ); |
| if ( $ident eq "OBSOLETE" ) { |
| # If id is "OBSOLETE", add a unique suffix. It provides convenient way to mark |
| # obsolete messages. |
| ++ $obsolete; |
| $ident .= $obsolete; |
| }; # if |
| if ( exists( $idents{ $ident } ) ) { |
| $error->( $n, $line, "Identifier \"$ident\" is redefined" ); |
| }; # if |
| # Check %-sequences. |
| my $err = parse_message( $message ); |
| if ( $err ) { |
| $error->( $n, $line, $err ); |
| }; # if |
| # Save message. |
| push( @{ $data->{ $section } }, [ $ident, $message ] ); |
| $idents{ $ident } = 1; |
| $last_idx = @{ $data->{ $section } } - 1; |
| next; |
| }; # if |
| # Parse continuation line. |
| if ( $line =~ m{\A\s*"(.*)"\s*\z} ) { |
| my $message = $1; |
| if ( not defined( $last_idx ) ) { |
| $error->( $n, $line, "Unexpected continuation line" ); |
| }; # if |
| # Check %-sequences. |
| my $err = parse_message( $message ); |
| if ( $err ) { |
| $error->( $n, $line, $err ); |
| }; # if |
| # Save continuation. |
| $data->{ $section }->[ $last_idx ]->[ 1 ] .= $message; |
| next; |
| }; # if |
| $error->( $n, $line, "Message definition expected" ); |
| }; # foreach |
| $data->{ "%meta" }->{ source } = $name; |
| foreach my $section ( @sections ) { |
| if ( not exists( $data->{ $section } ) ) { |
| $data->{ $section } = []; |
| }; # if |
| }; # foreach $section |
| |
| foreach my $property ( @properties ) { |
| if ( not defined( $data->{ "%meta" }->{ $property } ) ) { |
| runtime_error( |
| "Error parsing $name: " . |
| "Required \"$property\" property is not specified" |
| ); |
| }; # if |
| push( @{ $data->{ meta } }, [ $property, $data->{ "%meta" }->{ $property } ] ); |
| }; # foreach |
| |
| return $data; |
| |
| }; # sub parse_source |
| |
| |
| sub generate_enum($$$) { |
| |
| my ( $data, $file, $prefix ) = @_; |
| my $bulk = ""; |
| |
| $bulk = |
| _generate_comment( $data, "//", "//" ) . |
| "\n" . |
| "enum ${prefix}_id {\n\n" . |
| " // A special id for absence of message.\n" . |
| " ${prefix}_null = 0,\n\n"; |
| |
| foreach my $section ( @sections ) { |
| my $props = $sections->{ $section }; # Section properties. |
| my $short = $props->{ short }; # Short section name, frequently used. |
| $bulk .= |
| " // Set #$props->{ set }, $props->{ long }.\n" . |
| " ${prefix}_${short}_first = $props->{ base },\n"; |
| foreach my $item ( @{ $data->{ $section } } ) { |
| my ( $ident, undef ) = @$item; |
| $bulk .= " ${prefix}_${short}_${ident},\n"; |
| }; # foreach |
| $bulk .= " ${prefix}_${short}_last,\n\n"; |
| }; # foreach $type |
| $bulk .= " ${prefix}_xxx_lastest\n\n"; |
| |
| $bulk .= |
| "}; // enum ${prefix}_id\n" . |
| "\n" . |
| "typedef enum ${prefix}_id ${prefix}_id_t;\n" . |
| "\n"; |
| |
| $bulk .= |
| "\n" . |
| "// end of file //\n"; |
| |
| write_file( $file, \$bulk ); |
| |
| }; # sub generate_enum |
| |
| |
| sub generate_signature($$) { |
| |
| my ( $data, $file ) = @_; |
| my $bulk = ""; |
| |
| $bulk .= "// message catalog signature file //\n\n"; |
| |
| foreach my $section ( @sections ) { |
| my $props = $sections->{ $section }; # Section properties. |
| my $short = $props->{ short }; # Short section name, frequently used. |
| $bulk .= "-*- " . uc( $props->{ long } ) . "-*-\n\n"; |
| foreach my $item ( @{ $data->{ $section } } ) { |
| my ( $ident, $msg ) = @$item; |
| $bulk .= sprintf( "%-40s %s\n", $ident, msg2sgn( $msg ) ); |
| }; # foreach |
| $bulk .= "\n"; |
| }; # foreach $type |
| |
| $bulk .= "// end of file //\n"; |
| |
| write_file( $file, \$bulk ); |
| |
| }; # sub generate_signature |
| |
| |
| sub generate_default($$$) { |
| |
| my ( $data, $file, $prefix ) = @_; |
| my $bulk = ""; |
| |
| $bulk .= |
| _generate_comment( $data, "//", "//" ) . |
| "\n"; |
| |
| foreach my $section ( @sections ) { |
| $bulk .= |
| "static char const *\n" . |
| "__${prefix}_default_${section}" . "[] =\n" . |
| " {\n" . |
| " NULL,\n"; |
| foreach my $item ( @{ $data->{ $section } } ) { |
| my ( undef, $msg ) = @$item; |
| $bulk .= " \"" . msg2src( $msg ) . "\",\n"; |
| }; # while |
| $bulk .= |
| " NULL\n" . |
| " };\n" . |
| "\n"; |
| }; # foreach $type |
| |
| $bulk .= |
| "struct kmp_i18n_section {\n" . |
| " int size;\n" . |
| " char const ** str;\n" . |
| "}; // struct kmp_i18n_section\n" . |
| "typedef struct kmp_i18n_section kmp_i18n_section_t;\n" . |
| "\n" . |
| "static kmp_i18n_section_t\n" . |
| "__${prefix}_sections[] =\n" . |
| " {\n" . |
| " { 0, NULL },\n"; |
| foreach my $section ( @sections ) { |
| $bulk .= |
| " { " . @{ $data->{ $section } } . ", __${prefix}_default_${section} },\n"; |
| }; # foreach $type |
| $bulk .= |
| " { 0, NULL }\n" . |
| " };\n" . |
| "\n"; |
| |
| $bulk .= |
| "struct kmp_i18n_table {\n" . |
| " int size;\n" . |
| " kmp_i18n_section_t * sect;\n" . |
| "}; // struct kmp_i18n_table\n" . |
| "typedef struct kmp_i18n_table kmp_i18n_table_t;\n" . |
| "\n" . |
| "static kmp_i18n_table_t __kmp_i18n_default_table =\n" . |
| " {\n" . |
| " " . @sections . ",\n" . |
| " __kmp_i18n_sections\n" . |
| " };\n" . |
| "\n" . |
| "// end of file //\n"; |
| |
| write_file( $file, \$bulk ); |
| |
| }; # sub generate_default |
| |
| |
| sub generate_message_unix($$) { |
| |
| my ( $data, $file ) = @_; |
| my $bulk = ""; |
| |
| $bulk .= |
| _generate_comment( $data, "\$", "\$" ) . |
| "\n" . |
| "\$quote \"\n\n"; |
| |
| foreach my $section ( @sections ) { |
| $bulk .= |
| "\$ " . ( "-" x 78 ) . "\n\$ $section\n\$ " . ( "-" x 78 ) . "\n\n" . |
| "\$set $sections->{ $section }->{ set }\n" . |
| "\n"; |
| my $n = 0; |
| foreach my $item ( @{ $data->{ $section } } ) { |
| my ( undef, $msg ) = @$item; |
| ++ $n; |
| $bulk .= "$n \"" . msg2src( $msg ) . "\"\n"; |
| }; # foreach |
| $bulk .= "\n"; |
| }; # foreach $type |
| |
| $bulk .= |
| "\n" . |
| "\$ end of file \$\n"; |
| |
| write_file( $file, \$bulk, -layer => ":utf8" ); |
| |
| }; # sub generate_message_linux |
| |
| |
| sub generate_message_windows($$) { |
| |
| my ( $data, $file ) = @_; |
| my $bulk = ""; |
| my $language = $data->{ "%meta" }->{ Language }; |
| my $langid = $data->{ "%meta" }->{ LangId }; |
| |
| $bulk .= |
| _generate_comment( $data, ";", ";" ) . |
| "\n" . |
| "LanguageNames = ($language=$langid:msg_$langid)\n" . |
| "\n"; |
| |
| $bulk .= |
| "FacilityNames=(\n"; |
| foreach my $section ( @sections ) { |
| my $props = $sections->{ $section }; # Section properties. |
| $bulk .= |
| " $props->{ short }=" . $props->{ set } ."\n"; |
| }; # foreach $section |
| $bulk .= |
| ")\n\n"; |
| |
| foreach my $section ( @sections ) { |
| my $short = $sections->{ $section }->{ short }; |
| my $n = 0; |
| foreach my $item ( @{ $data->{ $section } } ) { |
| my ( undef, $msg ) = @$item; |
| ++ $n; |
| $bulk .= |
| "MessageId=$n\n" . |
| "Facility=$short\n" . |
| "Language=$language\n" . |
| msg2mc( $msg ) . "\n.\n\n"; |
| }; # foreach $item |
| }; # foreach $section |
| |
| $bulk .= |
| "\n" . |
| "; end of file ;\n"; |
| |
| $bulk = encode( "UTF-16LE", $bulk ); # Convert text to UTF-16LE used in Windows* OS. |
| write_file( $file, \$bulk, -binary => 1 ); |
| |
| }; # sub generate_message_windows |
| |
| |
| # |
| # Parse command line. |
| # |
| |
| my $input_file; |
| my $enum_file; |
| my $signature_file; |
| my $default_file; |
| my $message_file; |
| my $id; |
| my $prefix = ""; |
| get_options( |
| "os=s" => \$target_os, |
| "enum-file=s" => \$enum_file, |
| "signature-file=s" => \$signature_file, |
| "default-file=s" => \$default_file, |
| "message-file=s" => \$message_file, |
| "id|lang-id" => \$id, |
| "prefix=s" => \$prefix, |
| ); |
| if ( @ARGV == 0 ) { |
| cmdline_error( "No source file specified -- nothing to do" ); |
| }; # if |
| if ( @ARGV > 1 ) { |
| cmdline_error( "Too many source files specified" ); |
| }; # if |
| $input_file = $ARGV[ 0 ]; |
| |
| |
| my $generate_message; |
| if ( $target_os =~ m{\A(?:lin|mac)\z} ) { |
| $generate_message = \&generate_message_unix; |
| } elsif ( $target_os eq "win" ) { |
| $generate_message = \&generate_message_windows; |
| } else { |
| runtime_error( "OS \"$target_os\" is not supported" ); |
| }; # if |
| |
| |
| # |
| # Do the work. |
| # |
| |
| my $data = parse_source( $input_file ); |
| if ( defined( $id ) ) { |
| print( $data->{ "%meta" }->{ LangId }, "\n" ); |
| }; # if |
| if ( defined( $enum_file ) ) { |
| generate_enum( $data, $enum_file, $prefix ); |
| }; # if |
| if ( defined( $signature_file ) ) { |
| generate_signature( $data, $signature_file ); |
| }; # if |
| if ( defined( $default_file ) ) { |
| generate_default( $data, $default_file, $prefix ); |
| }; # if |
| if ( defined( $message_file ) ) { |
| $generate_message->( $data, $message_file ); |
| }; # if |
| |
| exit( 0 ); |
| |
| __END__ |
| |
| =pod |
| |
| =head1 NAME |
| |
| B<message-converter.pl> -- Convert message catalog source file into another text forms. |
| |
| =head1 SYNOPSIS |
| |
| B<message-converter.pl> I<option>... <file> |
| |
| =head1 OPTIONS |
| |
| =over |
| |
| =item B<--enum-file=>I<file> |
| |
| Generate enum file named I<file>. |
| |
| =item B<--default-file=>I<file> |
| |
| Generate default messages file named I<file>. |
| |
| =item B<--lang-id> |
| |
| Print language identifier of the message catalog source file. |
| |
| =item B<--message-file=>I<file> |
| |
| Generate message file. |
| |
| =item B<--signature-file=>I<file> |
| |
| Generate signature file. |
| |
| Signatures are used for checking compatibility. For example, to check a primary |
| catalog and its translation to another language, signatures of both catalogs should be generated |
| and compared. If signatures are identical, catalogs are compatible. |
| |
| =item B<--prefix=>I<prefix> |
| |
| Prefix to be used for all C identifiers (type and variable names) in enum and default messages |
| files. |
| |
| =item B<--os=>I<str> |
| |
| Specify OS name the message formats to be converted for. If not specified expolicitly, value of |
| LIBOMP_OS environment variable is used. If LIBOMP_OS is not defined, host OS is detected. |
| |
| Depending on OS, B<message-converter.pl> converts message formats to GNU style or MS style. |
| |
| =item Standard Options |
| |
| =over |
| |
| =item B<--doc> |
| |
| =item B<--manual> |
| |
| Print full documentation and exit. |
| |
| =item B<--help> |
| |
| Print short help message and exit. |
| |
| =item B<--version> |
| |
| Print version string and exit. |
| |
| =back |
| |
| =back |
| |
| =head1 ARGUMENTS |
| |
| =over |
| |
| =item I<file> |
| |
| A name of input file. |
| |
| =back |
| |
| =head1 DESCRIPTION |
| |
| =head2 Message Catalog File Format |
| |
| It is plain text file in UTF-8 encoding. Empty lines and lines beginning with sharp sign (C<#>) are |
| ignored. EBNF syntax of content: |
| |
| catalog = { section }; |
| section = header body; |
| header = "-*- " section-id " -*-" "\n"; |
| body = { message }; |
| message = message-id string "\n" { string "\n" }; |
| section-id = identifier; |
| message-id = "OBSOLETE" | identifier; |
| identifier = letter { letter | digit | "_" }; |
| string = """ { character } """; |
| |
| Identifier starts with letter, with following letters, digits, and underscores. Identifiers are |
| case-sensitive. Setion identifiers are fixed: C<META>, C<STRINGS>, C<FORMATS>, C<MESSAGES> and |
| C<HINTS>. Message identifiers must be unique within section. Special C<OBSOLETE> pseudo-identifier |
| may be used many times. |
| |
| String is a C string literal which must not cross line boundaries. |
| Long messages may occupy multiple lines, a string per line. |
| |
| Message may include printf-like GNU-style placeholders for arguments: C<%I<n>$I<t>>, |
| where I<n> is argument number (C<1>, C<2>, ...), |
| I<t> -- argument type, C<s> (string) or C<d> (32-bit integer). |
| |
| See also comments in F<i18n/en_US.txt>. |
| |
| =head2 Output Files |
| |
| This script can generate 3 different text files from single source: |
| |
| =over |
| |
| =item Enum file. |
| |
| Enum file is a C include file, containing definitions of message identifiers, e. g.: |
| |
| enum kmp_i18n_id { |
| |
| // Set #1, meta. |
| kmp_i18n_prp_first = 65536, |
| kmp_i18n_prp_Language, |
| kmp_i18n_prp_Country, |
| kmp_i18n_prp_LangId, |
| kmp_i18n_prp_Version, |
| kmp_i18n_prp_Revision, |
| kmp_i18n_prp_last, |
| |
| // Set #2, strings. |
| kmp_i18n_str_first = 131072, |
| kmp_i18n_str_Error, |
| kmp_i18n_str_UnknownFile, |
| kmp_i18n_str_NotANumber, |
| ... |
| |
| // Set #3, fotrmats. |
| ... |
| |
| kmp_i18n_xxx_lastest |
| |
| }; // enum kmp_i18n_id |
| |
| typedef enum kmp_i18n_id kmp_i18n_id_t; |
| |
| =item Default messages file. |
| |
| Default messages file is a C include file containing default messages to be embedded into |
| application (and used if external message catalog does not exist or could not be open): |
| |
| static char const * |
| __kmp_i18n_default_meta[] = |
| { |
| NULL, |
| "English", |
| "USA", |
| "1033", |
| "2", |
| "20090806", |
| NULL |
| }; |
| |
| static char const * |
| __kmp_i18n_default_strings[] = |
| { |
| "Error", |
| "(unknown file)", |
| "not a number", |
| ... |
| NULL |
| }; |
| |
| ... |
| |
| =item Message file. |
| |
| Message file is an input for message compiler, F<gencat> on Linux* OS and OS X*, or F<mc.exe> on |
| Windows* OS. |
| |
| Here is the example of Linux* OS message file: |
| |
| $quote " |
| 1 "Japanese" |
| 2 "Japan" |
| 3 "1041" |
| 4 "2" |
| 5 "Based on Enlish message catalog revision 20090806" |
| ... |
| |
| Example of Windows* OS message file: |
| |
| LanguageNames = (Japanese=10041:msg_1041) |
| |
| FacilityNames = ( |
| prp=1 |
| str=2 |
| fmt=3 |
| ... |
| ) |
| |
| MessageId=1 |
| Facility=prp |
| Language=Japanese |
| Japanese |
| . |
| |
| ... |
| |
| =item Signature. |
| |
| Signature is a processed source file: comments stripped, strings deleted, but placeholders kept and |
| sorted. |
| |
| -*- FORMATS-*- |
| |
| Info %1$d %2$s |
| Warning %1$d %2$s |
| Fatal %1$d %2$s |
| SysErr %1$d %2$s |
| Hint %1$- %2$s |
| Pragma %1$s %2$s %3$s %4$s |
| |
| The purpose of signatures -- compare two message source files for compatibility. If signatures of |
| two message sources are the same, binary message catalogs will be compatible. |
| |
| =back |
| |
| =head1 EXAMPLES |
| |
| Generate include file containing message identifiers: |
| |
| $ message-converter.pl --enum-file=kmp_i18n_id.inc en_US.txt |
| |
| Generate include file contating default messages: |
| |
| $ message-converter.pl --default-file=kmp_i18n_default.inc en_US.txt |
| |
| Generate input file for message compiler, Linux* OS example: |
| |
| $ message-converter.pl --message-file=ru_RU.UTF-8.msg ru_RU.txt |
| |
| Generate input file for message compiler, Windows* OS example: |
| |
| > message-converter.pl --message-file=ru_RU.UTF-8.mc ru_RU.txt |
| |
| =cut |
| |
| # end of file # |
| |