Blob Blame History Raw
/* Writing NeXTstep/GNUstep .strings files.
   Copyright (C) 2003, 2006-2008, 2015 Free Software Foundation, Inc.
   Written by Bruno Haible <bruno@clisp.org>, 2003.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

/* Specification.  */
#include "write-stringtable.h"

#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

#include "message.h"
#include "msgl-ascii.h"
#include "msgl-iconv.h"
#include "po-charset.h"
#include "c-strstr.h"
#include "ostream.h"
#include "xvasprintf.h"
#include "write-po.h"

/* The format of NeXTstep/GNUstep .strings files is documented in
     gnustep-base-1.8.0/Tools/make_strings/Using.txt
   and in the comments of method propertyListFromStringsFileFormat in
     gnustep-base-1.8.0/Source/NSString.m
   In summary, it's a Objective-C like file with pseudo-assignments of the form
          "key" = "value";
   where the key is the msgid and the value is the msgstr.
 */

/* Handling of comments: We copy all comments from the PO file to the
   .strings file. This is not really needed; it's a service for translators
   who don't like PO files and prefer to maintain the .strings file.  */

/* Since the interpretation of text files in GNUstep depends on the locale's
   encoding if they don't have a BOM, we choose one of three encodings with
   a BOM: UCS-2BE, UCS-2LE, UTF-8.  Since the first two of these don't cope
   with all of Unicode and we don't know whether GNUstep will switch to
   UTF-16 instead of UCS-2, we use UTF-8 with BOM.  BOMs are bad because they
   get in the way when concatenating files, but here we have no choice.  */

/* Writes a key or value to the stream, without newline.  */
static void
write_escaped_string (ostream_t stream, const char *str)
{
  const char *str_limit = str + strlen (str);

  ostream_write_str (stream, "\"");
  while (str < str_limit)
    {
      unsigned char c = (unsigned char) *str++;

      if (c == '\t')
        ostream_write_str (stream, "\\t");
      else if (c == '\n')
        ostream_write_str (stream, "\\n");
      else if (c == '\r')
        ostream_write_str (stream, "\\r");
      else if (c == '\f')
        ostream_write_str (stream, "\\f");
      else if (c == '\\' || c == '"')
        {
          char seq[2];
          seq[0] = '\\';
          seq[1] = c;
          ostream_write_mem (stream, seq, 2);
        }
      else
        {
          char seq[1];
          seq[0] = c;
          ostream_write_mem (stream, seq, 1);
        }
    }
  ostream_write_str (stream, "\"");
}

/* Writes a message to the stream.  */
static void
write_message (ostream_t stream, const message_ty *mp,
               size_t page_width, bool debug)
{
  /* Print translator comment if available.  */
  if (mp->comment != NULL)
    {
      size_t j;

      for (j = 0; j < mp->comment->nitems; ++j)
        {
          const char *s = mp->comment->item[j];

          /* Test whether it is safe to output the comment in C style, or
             whether we need C++ style for it.  */
          if (c_strstr (s, "*/") == NULL)
            {
              ostream_write_str (stream, "/*");
              if (*s != '\0' && *s != '\n')
                ostream_write_str (stream, " ");
              ostream_write_str (stream, s);
              ostream_write_str (stream, " */\n");
            }
          else
            do
              {
                const char *e;
                ostream_write_str (stream, "//");
                if (*s != '\0' && *s != '\n')
                  ostream_write_str (stream, " ");
                e = strchr (s, '\n');
                if (e == NULL)
                  {
                    ostream_write_str (stream, s);
                    s = NULL;
                  }
                else
                  {
                    ostream_write_mem (stream, s, e - s);
                    s = e + 1;
                  }
                ostream_write_str (stream, "\n");
              }
            while (s != NULL);
        }
    }

  /* Print xgettext extracted comments.  */
  if (mp->comment_dot != NULL)
    {
      size_t j;

      for (j = 0; j < mp->comment_dot->nitems; ++j)
        {
          const char *s = mp->comment_dot->item[j];

          /* Test whether it is safe to output the comment in C style, or
             whether we need C++ style for it.  */
          if (c_strstr (s, "*/") == NULL)
            {
              ostream_write_str (stream, "/* Comment: ");
              ostream_write_str (stream, s);
              ostream_write_str (stream, " */\n");
            }
          else
            {
              bool first = true;
              do
                {
                  const char *e;
                  ostream_write_str (stream, "//");
                  if (first || (*s != '\0' && *s != '\n'))
                    ostream_write_str (stream, " ");
                  if (first)
                    ostream_write_str (stream, "Comment: ");
                  e = strchr (s, '\n');
                  if (e == NULL)
                    {
                      ostream_write_str (stream, s);
                      s = NULL;
                    }
                  else
                    {
                      ostream_write_mem (stream, s, e - s);
                      s = e + 1;
                    }
                  ostream_write_str (stream, "\n");
                  first = false;
                }
              while (s != NULL);
            }
        }
    }

  /* Print the file position comments.  */
  if (mp->filepos_count != 0)
    {
      size_t j;

      for (j = 0; j < mp->filepos_count; ++j)
        {
          lex_pos_ty *pp = &mp->filepos[j];
          const char *cp = pp->file_name;
          char *str;

          while (cp[0] == '.' && cp[1] == '/')
            cp += 2;
          str = xasprintf ("/* File: %s:%ld */\n", cp, (long) pp->line_number);
          ostream_write_str (stream, str);
          free (str);
        }
    }

  /* Print flag information in special comment.  */
  if (mp->is_fuzzy || mp->msgstr[0] == '\0')
    ostream_write_str (stream, "/* Flag: untranslated */\n");
  if (mp->obsolete)
    ostream_write_str (stream, "/* Flag: unmatched */\n");
  {
    size_t i;
    for (i = 0; i < NFORMATS; i++)
      if (significant_format_p (mp->is_format[i]))
        {
          ostream_write_str (stream, "/* Flag: ");
          ostream_write_str (stream,
                             make_format_description_string (mp->is_format[i],
                                                             format_language[i],
                                                             debug));
          ostream_write_str (stream, " */\n");
        }
  }
  if (has_range_p (mp->range))
    {
      char *string;

      ostream_write_str (stream, "/* Flag: ");
      string = make_range_description_string (mp->range);
      ostream_write_str (stream, string);
      free (string);
      ostream_write_str (stream, " */\n");
    }

  /* Now write the untranslated string and the translated string.  */
  write_escaped_string (stream, mp->msgid);
  ostream_write_str (stream, " = ");
  if (mp->msgstr[0] != '\0')
    {
      if (mp->is_fuzzy)
        {
          /* Output the msgid as value, so that at runtime the untranslated
             string is returned.  */
          write_escaped_string (stream, mp->msgid);

          /* Output the msgstr as a comment, so that at runtime
             propertyListFromStringsFileFormat ignores it.  */
          if (c_strstr (mp->msgstr, "*/") == NULL)
            {
              ostream_write_str (stream, " /* = ");
              write_escaped_string (stream, mp->msgstr);
              ostream_write_str (stream, " */");
            }
          else
            {
              ostream_write_str (stream, "; // = ");
              write_escaped_string (stream, mp->msgstr);
            }
        }
      else
        write_escaped_string (stream, mp->msgstr);
    }
  else
    {
      /* Output the msgid as value, so that at runtime the untranslated
         string is returned.  */
      write_escaped_string (stream, mp->msgid);
    }
  ostream_write_str (stream, ";");

  ostream_write_str (stream, "\n");
}

/* Writes an entire message list to the stream.  */
static void
write_stringtable (ostream_t stream, message_list_ty *mlp,
                   const char *canon_encoding, size_t page_width, bool debug)
{
  bool blank_line;
  size_t j;

  /* Convert the messages to Unicode.  */
  iconv_message_list (mlp, canon_encoding, po_charset_utf8, NULL);

  /* Output the BOM.  */
  if (!is_ascii_message_list (mlp))
    ostream_write_str (stream, "\xef\xbb\xbf");

  /* Loop through the messages.  */
  blank_line = false;
  for (j = 0; j < mlp->nitems; ++j)
    {
      const message_ty *mp = mlp->item[j];

      if (mp->msgid_plural == NULL)
        {
          if (blank_line)
            ostream_write_str (stream, "\n");

          write_message (stream, mp, page_width, debug);

          blank_line = true;
        }
    }
}

/* Output the contents of a PO file in .strings syntax.  */
static void
msgdomain_list_print_stringtable (msgdomain_list_ty *mdlp, ostream_t stream,
                                  size_t page_width, bool debug)
{
  message_list_ty *mlp;

  if (mdlp->nitems == 1)
    mlp = mdlp->item[0]->messages;
  else
    mlp = message_list_alloc (false);
  write_stringtable (stream, mlp, mdlp->encoding, page_width, debug);
}

/* Describes a PO file in .strings syntax.  */
const struct catalog_output_format output_format_stringtable =
{
  msgdomain_list_print_stringtable,     /* print */
  true,                                 /* requires_utf8 */
  false,                                /* supports_color */
  false,                                /* supports_multiple_domains */
  false,                                /* supports_contexts */
  false,                                /* supports_plurals */
  false,                                /* sorts_obsoletes_to_end */
  false,                                /* alternative_is_po */
  false                                 /* alternative_is_java_class */
};