| /* Initializes a new PO file. |
| Copyright (C) 2001-2020 Free Software Foundation, Inc. |
| Written by Bruno Haible <haible@clisp.cons.org>, 2001. |
| |
| 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 <https://www.gnu.org/licenses/>. */ |
| |
| |
| #ifdef HAVE_CONFIG_H |
| # include "config.h" |
| #endif |
| #include <alloca.h> |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <getopt.h> |
| #include <limits.h> |
| #include <locale.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <time.h> |
| #include <sys/types.h> |
| |
| #if HAVE_PWD_H |
| # include <pwd.h> |
| #endif |
| |
| #include <unistd.h> |
| |
| #if HAVE_DIRENT_H |
| # include <dirent.h> |
| #endif |
| |
| #if HAVE_DIRENT_H |
| # define HAVE_DIR 1 |
| #else |
| # define HAVE_DIR 0 |
| #endif |
| |
| #include <textstyle.h> |
| |
| /* Get BINDIR. */ |
| #include "configmake.h" |
| |
| #include "noreturn.h" |
| #include "closeout.h" |
| #include "error.h" |
| #include "error-progname.h" |
| #include "progname.h" |
| #include "relocatable.h" |
| #include "basename-lgpl.h" |
| #include "c-strstr.h" |
| #include "c-strcase.h" |
| #include "message.h" |
| #include "read-catalog.h" |
| #include "read-po.h" |
| #include "read-properties.h" |
| #include "read-stringtable.h" |
| #include "write-catalog.h" |
| #include "write-po.h" |
| #include "write-properties.h" |
| #include "write-stringtable.h" |
| #include "po-charset.h" |
| #include "localcharset.h" |
| #include "localename.h" |
| #include "po-time.h" |
| #include "plural-table.h" |
| #include "lang-table.h" |
| #include "xalloc.h" |
| #include "xmalloca.h" |
| #include "concat-filename.h" |
| #include "xerror.h" |
| #include "xvasprintf.h" |
| #include "msgl-english.h" |
| #include "plural-count.h" |
| #include "spawn-pipe.h" |
| #include "wait-process.h" |
| #include "xsetenv.h" |
| #include "str-list.h" |
| #include "propername.h" |
| #include "gettext.h" |
| |
| #define _(str) gettext (str) |
| #define N_(str) (str) |
| |
| /* Get F_OK. It is lacking from <fcntl.h> on Woe32. */ |
| #ifndef F_OK |
| # define F_OK 0 |
| #endif |
| |
| #define SIZEOF(a) (sizeof(a) / sizeof(a[0])) |
| |
| extern const char * _nl_expand_alias (const char *name); |
| |
| /* Locale name. */ |
| static const char *locale; |
| |
| /* Language (ISO-639 code) and optional territory (ISO-3166 code). */ |
| static const char *catalogname; |
| |
| /* Language (ISO-639 code). */ |
| static const char *language; |
| |
| /* If true, the user is not considered to be the translator. */ |
| static bool no_translator; |
| |
| /* Long options. */ |
| static const struct option long_options[] = |
| { |
| { "color", optional_argument, NULL, CHAR_MAX + 5 }, |
| { "help", no_argument, NULL, 'h' }, |
| { "input", required_argument, NULL, 'i' }, |
| { "locale", required_argument, NULL, 'l' }, |
| { "no-translator", no_argument, NULL, CHAR_MAX + 1 }, |
| { "no-wrap", no_argument, NULL, CHAR_MAX + 2 }, |
| { "output-file", required_argument, NULL, 'o' }, |
| { "properties-input", no_argument, NULL, 'P' }, |
| { "properties-output", no_argument, NULL, 'p' }, |
| { "stringtable-input", no_argument, NULL, CHAR_MAX + 3 }, |
| { "stringtable-output", no_argument, NULL, CHAR_MAX + 4 }, |
| { "style", required_argument, NULL, CHAR_MAX + 6 }, |
| { "version", no_argument, NULL, 'V' }, |
| { "width", required_argument, NULL, 'w' }, |
| { NULL, 0, NULL, 0 } |
| }; |
| |
| /* Forward declaration of local functions. */ |
| _GL_NORETURN_FUNC static void usage (int status); |
| static const char *find_pot (void); |
| static const char *catalogname_for_locale (const char *locale); |
| static const char *language_of_locale (const char *locale); |
| static char *get_field (const char *header, const char *field); |
| static msgdomain_list_ty *fill_header (msgdomain_list_ty *mdlp); |
| static msgdomain_list_ty *update_msgstr_plurals (msgdomain_list_ty *mdlp); |
| |
| |
| int |
| main (int argc, char **argv) |
| { |
| int opt; |
| bool do_help; |
| bool do_version; |
| char *output_file; |
| const char *input_file; |
| msgdomain_list_ty *result; |
| catalog_input_format_ty input_syntax = &input_format_po; |
| catalog_output_format_ty output_syntax = &output_format_po; |
| |
| /* Set program name for messages. */ |
| set_program_name (argv[0]); |
| error_print_progname = maybe_print_progname; |
| |
| /* Set locale via LC_ALL. */ |
| setlocale (LC_ALL, ""); |
| |
| /* Set the text message domain. */ |
| bindtextdomain (PACKAGE, relocate (LOCALEDIR)); |
| bindtextdomain ("bison-runtime", relocate (BISON_LOCALEDIR)); |
| textdomain (PACKAGE); |
| |
| /* Ensure that write errors on stdout are detected. */ |
| atexit (close_stdout); |
| |
| /* Set default values for variables. */ |
| do_help = false; |
| do_version = false; |
| output_file = NULL; |
| input_file = NULL; |
| locale = NULL; |
| |
| while ((opt = getopt_long (argc, argv, "hi:l:o:pPVw:", long_options, NULL)) |
| != EOF) |
| switch (opt) |
| { |
| case '\0': /* Long option. */ |
| break; |
| |
| case 'h': |
| do_help = true; |
| break; |
| |
| case 'i': |
| if (input_file != NULL) |
| { |
| error (EXIT_SUCCESS, 0, _("at most one input file allowed")); |
| usage (EXIT_FAILURE); |
| } |
| input_file = optarg; |
| break; |
| |
| case 'l': |
| locale = optarg; |
| break; |
| |
| case 'o': |
| output_file = optarg; |
| break; |
| |
| case 'p': |
| output_syntax = &output_format_properties; |
| break; |
| |
| case 'P': |
| input_syntax = &input_format_properties; |
| break; |
| |
| case 'V': |
| do_version = true; |
| break; |
| |
| case 'w': |
| { |
| int value; |
| char *endp; |
| value = strtol (optarg, &endp, 10); |
| if (endp != optarg) |
| message_page_width_set (value); |
| } |
| break; |
| |
| case CHAR_MAX + 1: |
| no_translator = true; |
| break; |
| |
| case CHAR_MAX + 2: /* --no-wrap */ |
| message_page_width_ignore (); |
| break; |
| |
| case CHAR_MAX + 3: /* --stringtable-input */ |
| input_syntax = &input_format_stringtable; |
| break; |
| |
| case CHAR_MAX + 4: /* --stringtable-output */ |
| output_syntax = &output_format_stringtable; |
| break; |
| |
| case CHAR_MAX + 5: /* --color */ |
| if (handle_color_option (optarg) || color_test_mode) |
| usage (EXIT_FAILURE); |
| break; |
| |
| case CHAR_MAX + 6: /* --style */ |
| handle_style_option (optarg); |
| break; |
| |
| default: |
| usage (EXIT_FAILURE); |
| break; |
| } |
| |
| /* Version information is requested. */ |
| if (do_version) |
| { |
| printf ("%s (GNU %s) %s\n", last_component (program_name), |
| PACKAGE, VERSION); |
| /* xgettext: no-wrap */ |
| printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\ |
| License GPLv3+: GNU GPL version 3 or later <%s>\n\ |
| This is free software: you are free to change and redistribute it.\n\ |
| There is NO WARRANTY, to the extent permitted by law.\n\ |
| "), |
| "2001-2020", "https://gnu.org/licenses/gpl.html"); |
| printf (_("Written by %s.\n"), proper_name ("Bruno Haible")); |
| exit (EXIT_SUCCESS); |
| } |
| |
| /* Help is requested. */ |
| if (do_help) |
| usage (EXIT_SUCCESS); |
| |
| /* Test for extraneous arguments. */ |
| if (optind != argc) |
| error (EXIT_FAILURE, 0, _("too many arguments")); |
| |
| /* Search for the input file. */ |
| if (input_file == NULL) |
| input_file = find_pot (); |
| |
| /* Determine target locale. */ |
| if (locale == NULL) |
| { |
| locale = gl_locale_name (LC_MESSAGES, "LC_MESSAGES"); |
| if (strcmp (locale, "C") == 0) |
| { |
| const char *doc_url = |
| "https://www.gnu.org/software/gettext/manual/html_node/Setting-the-POSIX-Locale.html"; |
| multiline_error (xstrdup (""), |
| xasprintf (_("\ |
| You are in a language indifferent environment. Please set\n\ |
| your LANG environment variable, as described in\n\ |
| <%s>.\n\ |
| This is necessary so you can test your translations.\n"), |
| doc_url)); |
| exit (EXIT_FAILURE); |
| } |
| } |
| { |
| const char *alias = _nl_expand_alias (locale); |
| if (alias != NULL) |
| locale = alias; |
| } |
| catalogname = catalogname_for_locale (locale); |
| language = language_of_locale (locale); |
| |
| /* Default output file name is CATALOGNAME.po. */ |
| if (output_file == NULL) |
| { |
| output_file = xasprintf ("%s.po", catalogname); |
| |
| /* But don't overwrite existing PO files. */ |
| if (access (output_file, F_OK) == 0) |
| { |
| multiline_error (xstrdup (""), |
| xasprintf (_("\ |
| Output file %s already exists.\n\ |
| Please specify the locale through the --locale option or\n\ |
| the output .po file through the --output-file option.\n"), |
| output_file)); |
| exit (EXIT_FAILURE); |
| } |
| } |
| |
| /* Read input file. */ |
| result = read_catalog_file (input_file, input_syntax); |
| |
| #if defined _WIN32 || defined __CYGWIN__ |
| /* The function fill_header invokes, directly or indirectly, some programs |
| that are installed in ${libdir}/gettext: |
| - hostname, invoked indirectly through 'user-email'. |
| - urlget, invoked indirectly through 'team-address'. |
| - cldr-plurals, invoked directly. |
| These programs depend on libintl. In installations with shared libraries, |
| we need to guarantee that the programs find the DLL, which is installed |
| in ${bindir}, not in ${libdir}/gettext. The preferred way to do so is to |
| extend $PATH, so that it contains ${bindir}. */ |
| { |
| const char *orig_path; |
| size_t orig_path_len; |
| char separator; |
| const char *bindir; |
| size_t bindir_len; |
| char *augmented_path; |
| |
| orig_path = getenv ("PATH"); |
| if (orig_path == NULL) |
| orig_path = ""; |
| orig_path_len = strlen (orig_path); |
| |
| #if defined __CYGWIN__ |
| separator = ':'; |
| #else /* native Windows */ |
| separator = ';'; |
| #endif |
| |
| bindir = BINDIR; |
| bindir_len = strlen (bindir); |
| |
| /* Concatenate bindir, separator, orig_path. */ |
| augmented_path = XNMALLOC (bindir_len + 1 + orig_path_len + 1, char); |
| memcpy (augmented_path, bindir, bindir_len); |
| augmented_path[bindir_len] = separator; |
| memcpy (augmented_path + bindir_len + 1, orig_path, orig_path_len + 1); |
| |
| xsetenv ("PATH", augmented_path, 1); |
| } |
| #endif |
| |
| /* Fill the header entry. */ |
| result = fill_header (result); |
| |
| /* Initialize translations. */ |
| if (strcmp (language, "en") == 0) |
| result = msgdomain_list_english (result); |
| else |
| result = update_msgstr_plurals (result); |
| |
| /* Write the modified message list out. */ |
| msgdomain_list_print (result, output_file, output_syntax, true, false); |
| |
| if (!no_translator) |
| fprintf (stderr, "\n"); |
| fprintf (stderr, _("Created %s.\n"), output_file); |
| |
| exit (EXIT_SUCCESS); |
| } |
| |
| |
| /* Display usage information and exit. */ |
| static void |
| usage (int status) |
| { |
| if (status != EXIT_SUCCESS) |
| fprintf (stderr, _("Try '%s --help' for more information.\n"), |
| program_name); |
| else |
| { |
| printf (_("\ |
| Usage: %s [OPTION]\n\ |
| "), program_name); |
| printf ("\n"); |
| /* xgettext: no-wrap */ |
| printf (_("\ |
| Creates a new PO file, initializing the meta information with values from the\n\ |
| user's environment.\n\ |
| ")); |
| printf ("\n"); |
| printf (_("\ |
| Mandatory arguments to long options are mandatory for short options too.\n")); |
| printf ("\n"); |
| printf (_("\ |
| Input file location:\n")); |
| printf (_("\ |
| -i, --input=INPUTFILE input POT file\n")); |
| printf (_("\ |
| If no input file is given, the current directory is searched for the POT file.\n\ |
| If it is -, standard input is read.\n")); |
| printf ("\n"); |
| printf (_("\ |
| Output file location:\n")); |
| printf (_("\ |
| -o, --output-file=FILE write output to specified PO file\n")); |
| printf (_("\ |
| If no output file is given, it depends on the --locale option or the user's\n\ |
| locale setting. If it is -, the results are written to standard output.\n")); |
| printf ("\n"); |
| printf (_("\ |
| Input file syntax:\n")); |
| printf (_("\ |
| -P, --properties-input input file is in Java .properties syntax\n")); |
| printf (_("\ |
| --stringtable-input input file is in NeXTstep/GNUstep .strings syntax\n")); |
| printf ("\n"); |
| printf (_("\ |
| Output details:\n")); |
| printf (_("\ |
| -l, --locale=LL_CC[.ENCODING] set target locale\n")); |
| printf (_("\ |
| --no-translator assume the PO file is automatically generated\n")); |
| printf (_("\ |
| --color use colors and other text attributes always\n\ |
| --color=WHEN use colors and other text attributes if WHEN.\n\ |
| WHEN may be 'always', 'never', 'auto', or 'html'.\n")); |
| printf (_("\ |
| --style=STYLEFILE specify CSS style rule file for --color\n")); |
| printf (_("\ |
| -p, --properties-output write out a Java .properties file\n")); |
| printf (_("\ |
| --stringtable-output write out a NeXTstep/GNUstep .strings file\n")); |
| printf (_("\ |
| -w, --width=NUMBER set output page width\n")); |
| printf (_("\ |
| --no-wrap do not break long message lines, longer than\n\ |
| the output page width, into several lines\n")); |
| printf ("\n"); |
| printf (_("\ |
| Informative output:\n")); |
| printf (_("\ |
| -h, --help display this help and exit\n")); |
| printf (_("\ |
| -V, --version output version information and exit\n")); |
| printf ("\n"); |
| /* TRANSLATORS: The first placeholder is the web address of the Savannah |
| project of this package. The second placeholder is the bug-reporting |
| email address for this package. Please add _another line_ saying |
| "Report translation bugs to <...>\n" with the address for translation |
| bugs (typically your translation team's web or email address). */ |
| printf(_("\ |
| Report bugs in the bug tracker at <%s>\n\ |
| or by email to <%s>.\n"), |
| "https://savannah.gnu.org/projects/gettext", |
| "bug-gettext@gnu.org"); |
| } |
| |
| exit (status); |
| } |
| |
| |
| /* Search for the POT file and return its name. */ |
| static const char * |
| find_pot () |
| { |
| #if HAVE_DIR |
| DIR *dirp; |
| char *found = NULL; |
| |
| dirp = opendir ("."); |
| if (dirp != NULL) |
| { |
| for (;;) |
| { |
| struct dirent *dp; |
| |
| errno = 0; |
| dp = readdir (dirp); |
| if (dp != NULL) |
| { |
| const char *name = dp->d_name; |
| size_t namlen = strlen (name); |
| |
| if (namlen > 4 && memcmp (name + namlen - 4, ".pot", 4) == 0) |
| { |
| if (found == NULL) |
| found = xstrdup (name); |
| else |
| { |
| multiline_error (xstrdup (""), |
| xstrdup (_("\ |
| Found more than one .pot file.\n\ |
| Please specify the input .pot file through the --input option.\n"))); |
| usage (EXIT_FAILURE); |
| } |
| } |
| } |
| else if (errno != 0) |
| error (EXIT_FAILURE, errno, _("error reading current directory")); |
| else |
| break; |
| } |
| if (closedir (dirp)) |
| error (EXIT_FAILURE, errno, _("error reading current directory")); |
| |
| if (found != NULL) |
| return found; |
| } |
| #endif |
| |
| multiline_error (xstrdup (""), |
| xstrdup (_("\ |
| Found no .pot file in the current directory.\n\ |
| Please specify the input .pot file through the --input option.\n"))); |
| usage (EXIT_FAILURE); |
| /* NOTREACHED */ |
| return NULL; |
| } |
| |
| |
| /* Return the gettext catalog name corresponding to a locale. If the locale |
| consists of a language and a territory, and the language is mainly spoken |
| in that territory, the territory is removed from the locale name. |
| For example, "de_DE" or "de_DE.ISO-8859-1" are simplified to "de", |
| because the resulting catalog can be used as a default for all "de_XX", |
| such as "de_AT". */ |
| static const char * |
| catalogname_for_locale (const char *locale) |
| { |
| static const char *locales_with_principal_territory[] = { |
| /* Language Main territory */ |
| "ace_ID", /* Achinese Indonesia */ |
| "af_ZA", /* Afrikaans South Africa */ |
| "ak_GH", /* Akan Ghana */ |
| "am_ET", /* Amharic Ethiopia */ |
| "an_ES", /* Aragonese Spain */ |
| "ang_GB", /* Old English Britain */ |
| "arn_CL", /* Mapudungun Chile */ |
| "as_IN", /* Assamese India */ |
| "ast_ES", /* Asturian Spain */ |
| "av_RU", /* Avaric Russia */ |
| "awa_IN", /* Awadhi India */ |
| "az_AZ", /* Azerbaijani Azerbaijan */ |
| "ban_ID", /* Balinese Indonesia */ |
| "be_BY", /* Belarusian Belarus */ |
| "bej_SD", /* Beja Sudan */ |
| "bem_ZM", /* Bemba Zambia */ |
| "bg_BG", /* Bulgarian Bulgaria */ |
| "bho_IN", /* Bhojpuri India */ |
| "bi_VU", /* Bislama Vanuatu */ |
| "bik_PH", /* Bikol Philippines */ |
| "bin_NG", /* Bini Nigeria */ |
| "bm_ML", /* Bambara Mali */ |
| "bn_IN", /* Bengali India */ |
| "bo_CN", /* Tibetan China */ |
| "br_FR", /* Breton France */ |
| "bs_BA", /* Bosnian Bosnia */ |
| "bug_ID", /* Buginese Indonesia */ |
| "ca_ES", /* Catalan Spain */ |
| "ce_RU", /* Chechen Russia */ |
| "ceb_PH", /* Cebuano Philippines */ |
| "co_FR", /* Corsican France */ |
| "cr_CA", /* Cree Canada */ |
| /* Don't put "crh_UZ" or "crh_UA" here. That would be asking for fruitless |
| political discussion. */ |
| "cs_CZ", /* Czech Czech Republic */ |
| "csb_PL", /* Kashubian Poland */ |
| "cy_GB", /* Welsh Britain */ |
| "da_DK", /* Danish Denmark */ |
| "de_DE", /* German Germany */ |
| "din_SD", /* Dinka Sudan */ |
| "doi_IN", /* Dogri India */ |
| "dsb_DE", /* Lower Sorbian Germany */ |
| "dv_MV", /* Divehi Maldives */ |
| "dz_BT", /* Dzongkha Bhutan */ |
| "ee_GH", /* Éwé Ghana */ |
| "el_GR", /* Greek Greece */ |
| /* Don't put "en_GB" or "en_US" here. That would be asking for fruitless |
| political discussion. */ |
| "es_ES", /* Spanish Spain */ |
| "et_EE", /* Estonian Estonia */ |
| "fa_IR", /* Persian Iran */ |
| "fi_FI", /* Finnish Finland */ |
| "fil_PH", /* Filipino Philippines */ |
| "fj_FJ", /* Fijian Fiji */ |
| "fo_FO", /* Faroese Faeroe Islands */ |
| "fon_BJ", /* Fon Benin */ |
| "fr_FR", /* French France */ |
| "fur_IT", /* Friulian Italy */ |
| "fy_NL", /* Western Frisian Netherlands */ |
| "ga_IE", /* Irish Ireland */ |
| "gd_GB", /* Scottish Gaelic Britain */ |
| "gon_IN", /* Gondi India */ |
| "gsw_CH", /* Swiss German Switzerland */ |
| "gu_IN", /* Gujarati India */ |
| "he_IL", /* Hebrew Israel */ |
| "hi_IN", /* Hindi India */ |
| "hil_PH", /* Hiligaynon Philippines */ |
| "hr_HR", /* Croatian Croatia */ |
| "hsb_DE", /* Upper Sorbian Germany */ |
| "ht_HT", /* Haitian Haiti */ |
| "hu_HU", /* Hungarian Hungary */ |
| "hy_AM", /* Armenian Armenia */ |
| "id_ID", /* Indonesian Indonesia */ |
| "ig_NG", /* Igbo Nigeria */ |
| "ii_CN", /* Sichuan Yi China */ |
| "ilo_PH", /* Iloko Philippines */ |
| "is_IS", /* Icelandic Iceland */ |
| "it_IT", /* Italian Italy */ |
| "ja_JP", /* Japanese Japan */ |
| "jab_NG", /* Hyam Nigeria */ |
| "jv_ID", /* Javanese Indonesia */ |
| "ka_GE", /* Georgian Georgia */ |
| "kab_DZ", /* Kabyle Algeria */ |
| "kaj_NG", /* Jju Nigeria */ |
| "kam_KE", /* Kamba Kenya */ |
| "kmb_AO", /* Kimbundu Angola */ |
| "kcg_NG", /* Tyap Nigeria */ |
| "kdm_NG", /* Kagoma Nigeria */ |
| "kg_CD", /* Kongo Democratic Republic of Congo */ |
| "kk_KZ", /* Kazakh Kazakhstan */ |
| "kl_GL", /* Kalaallisut Greenland */ |
| "km_KH", /* Central Khmer Cambodia */ |
| "kn_IN", /* Kannada India */ |
| "ko_KR", /* Korean Korea (South) */ |
| "kok_IN", /* Konkani India */ |
| "kr_NG", /* Kanuri Nigeria */ |
| "kru_IN", /* Kurukh India */ |
| "ky_KG", /* Kyrgyz Kyrgyzstan */ |
| "lg_UG", /* Ganda Uganda */ |
| "li_BE", /* Limburgish Belgium */ |
| "lo_LA", /* Laotian Laos */ |
| "lt_LT", /* Lithuanian Lithuania */ |
| "lu_CD", /* Luba-Katanga Democratic Republic of Congo */ |
| "lua_CD", /* Luba-Lulua Democratic Republic of Congo */ |
| "luo_KE", /* Luo Kenya */ |
| "lv_LV", /* Latvian Latvia */ |
| "mad_ID", /* Madurese Indonesia */ |
| "mag_IN", /* Magahi India */ |
| "mai_IN", /* Maithili India */ |
| "mak_ID", /* Makasar Indonesia */ |
| "man_ML", /* Mandingo Mali */ |
| "men_SL", /* Mende Sierra Leone */ |
| "mfe_MU", /* Mauritian Creole Mauritius */ |
| "mg_MG", /* Malagasy Madagascar */ |
| "mi_NZ", /* Maori New Zealand */ |
| "min_ID", /* Minangkabau Indonesia */ |
| "mk_MK", /* Macedonian North Macedonia */ |
| "ml_IN", /* Malayalam India */ |
| "mn_MN", /* Mongolian Mongolia */ |
| "mni_IN", /* Manipuri India */ |
| "mos_BF", /* Mossi Burkina Faso */ |
| "mr_IN", /* Marathi India */ |
| "ms_MY", /* Malay Malaysia */ |
| "mt_MT", /* Maltese Malta */ |
| "mwr_IN", /* Marwari India */ |
| "my_MM", /* Burmese Myanmar */ |
| "na_NR", /* Nauru Nauru */ |
| "nah_MX", /* Nahuatl Mexico */ |
| "nap_IT", /* Neapolitan Italy */ |
| "nb_NO", /* Norwegian Bokmål Norway */ |
| "nds_DE", /* Low Saxon Germany */ |
| "ne_NP", /* Nepali Nepal */ |
| "nl_NL", /* Dutch Netherlands */ |
| "nn_NO", /* Norwegian Nynorsk Norway */ |
| "no_NO", /* Norwegian Norway */ |
| "nr_ZA", /* South Ndebele South Africa */ |
| "nso_ZA", /* Northern Sotho South Africa */ |
| "ny_MW", /* Chichewa Malawi */ |
| "nym_TZ", /* Nyamwezi Tanzania */ |
| "nyn_UG", /* Nyankole Uganda */ |
| "oc_FR", /* Occitan France */ |
| "oj_CA", /* Ojibwa Canada */ |
| "or_IN", /* Oriya India */ |
| "pa_IN", /* Punjabi India */ |
| "pag_PH", /* Pangasinan Philippines */ |
| "pam_PH", /* Pampanga Philippines */ |
| "pap_AN", /* Papiamento Netherlands Antilles - this line can be removed in 2018 */ |
| "pbb_CO", /* Páez Colombia */ |
| "pl_PL", /* Polish Poland */ |
| "ps_AF", /* Pashto Afghanistan */ |
| "pt_PT", /* Portuguese Portugal */ |
| "raj_IN", /* Rajasthani India */ |
| "rm_CH", /* Romansh Switzerland */ |
| "rn_BI", /* Kirundi Burundi */ |
| "ro_RO", /* Romanian Romania */ |
| "ru_RU", /* Russian Russia */ |
| "rw_RW", /* Kinyarwanda Rwanda */ |
| "sa_IN", /* Sanskrit India */ |
| "sah_RU", /* Yakut Russia */ |
| "sas_ID", /* Sasak Indonesia */ |
| "sat_IN", /* Santali India */ |
| "sc_IT", /* Sardinian Italy */ |
| "scn_IT", /* Sicilian Italy */ |
| "sg_CF", /* Sango Central African Republic */ |
| "shn_MM", /* Shan Myanmar */ |
| "si_LK", /* Sinhala Sri Lanka */ |
| "sid_ET", /* Sidamo Ethiopia */ |
| "sk_SK", /* Slovak Slovakia */ |
| "sl_SI", /* Slovenian Slovenia */ |
| "smn_FI", /* Inari Sami Finland */ |
| "sms_FI", /* Skolt Sami Finland */ |
| "so_SO", /* Somali Somalia */ |
| "sq_AL", /* Albanian Albania */ |
| "sr_RS", /* Serbian Serbia */ |
| "srr_SN", /* Serer Senegal */ |
| "suk_TZ", /* Sukuma Tanzania */ |
| "sus_GN", /* Susu Guinea */ |
| "sv_SE", /* Swedish Sweden */ |
| "te_IN", /* Telugu India */ |
| "tem_SL", /* Timne Sierra Leone */ |
| "tet_ID", /* Tetum Indonesia */ |
| "tg_TJ", /* Tajik Tajikistan */ |
| "th_TH", /* Thai Thailand */ |
| "tiv_NG", /* Tiv Nigeria */ |
| "tk_TM", /* Turkmen Turkmenistan */ |
| "tl_PH", /* Tagalog Philippines */ |
| "to_TO", /* Tonga Tonga */ |
| "tpi_PG", /* Tok Pisin Papua New Guinea */ |
| "tr_TR", /* Turkish Turkey */ |
| "tum_MW", /* Tumbuka Malawi */ |
| "ug_CN", /* Uighur China */ |
| "uk_UA", /* Ukrainian Ukraine */ |
| "umb_AO", /* Umbundu Angola */ |
| "ur_PK", /* Urdu Pakistan */ |
| "uz_UZ", /* Uzbek Uzbekistan */ |
| "ve_ZA", /* Venda South Africa */ |
| "vi_VN", /* Vietnamese Vietnam */ |
| "wa_BE", /* Walloon Belgium */ |
| "wal_ET", /* Walamo Ethiopia */ |
| "war_PH", /* Waray Philippines */ |
| "wen_DE", /* Sorbian Germany */ |
| "yao_MW", /* Yao Malawi */ |
| "zap_MX" /* Zapotec Mexico */ |
| }; |
| const char *dot; |
| size_t i; |
| |
| /* Remove the ".codeset" part from the locale. */ |
| dot = strchr (locale, '.'); |
| if (dot != NULL) |
| { |
| const char *codeset_end; |
| char *shorter_locale; |
| |
| codeset_end = strpbrk (dot + 1, "_@"); |
| if (codeset_end == NULL) |
| codeset_end = dot + strlen (dot); |
| |
| shorter_locale = XNMALLOC (strlen (locale), char); |
| memcpy (shorter_locale, locale, dot - locale); |
| strcpy (shorter_locale + (dot - locale), codeset_end); |
| locale = shorter_locale; |
| } |
| |
| /* If the territory is the language's principal territory, drop it. */ |
| for (i = 0; i < SIZEOF (locales_with_principal_territory); i++) |
| if (strcmp (locale, locales_with_principal_territory[i]) == 0) |
| { |
| const char *language_end; |
| size_t len; |
| char *shorter_locale; |
| |
| language_end = strchr (locale, '_'); |
| if (language_end == NULL) |
| abort (); |
| |
| len = language_end - locale; |
| shorter_locale = XNMALLOC (len + 1, char); |
| memcpy (shorter_locale, locale, len); |
| shorter_locale[len] = '\0'; |
| locale = shorter_locale; |
| break; |
| } |
| |
| return locale; |
| } |
| |
| |
| /* Return the language of a locale. */ |
| static const char * |
| language_of_locale (const char *locale) |
| { |
| const char *language_end; |
| |
| language_end = strpbrk (locale, "_.@"); |
| if (language_end != NULL) |
| { |
| size_t len; |
| char *result; |
| |
| len = language_end - locale; |
| result = XNMALLOC (len + 1, char); |
| memcpy (result, locale, len); |
| result[len] = '\0'; |
| |
| return result; |
| } |
| else |
| return locale; |
| } |
| |
| |
| /* Return the most likely desired charset for the PO file, as a portable |
| charset name. */ |
| static const char * |
| canonical_locale_charset () |
| { |
| const char *tmp; |
| char *old_LC_ALL; |
| const char *charset; |
| |
| /* Save LC_ALL environment variable. */ |
| |
| tmp = getenv ("LC_ALL"); |
| old_LC_ALL = (tmp != NULL ? xstrdup (tmp) : NULL); |
| |
| xsetenv ("LC_ALL", locale, 1); |
| |
| if (setlocale (LC_ALL, "") == NULL) |
| /* Nonexistent locale. Use anything. */ |
| charset = ""; |
| else |
| /* Get the locale's charset. */ |
| charset = locale_charset (); |
| |
| /* Restore LC_ALL environment variable. */ |
| |
| if (old_LC_ALL != NULL) |
| xsetenv ("LC_ALL", old_LC_ALL, 1), free (old_LC_ALL); |
| else |
| unsetenv ("LC_ALL"); |
| |
| setlocale (LC_ALL, ""); |
| |
| /* Canonicalize it. */ |
| charset = po_charset_canonicalize (charset); |
| if (charset == NULL) |
| charset = po_charset_ascii; |
| |
| return charset; |
| } |
| |
| |
| /* Return the English name of the language. */ |
| static const char * |
| englishname_of_language () |
| { |
| size_t i; |
| |
| for (i = 0; i < language_table_size; i++) |
| if (strcmp (language_table[i].code, language) == 0) |
| return language_table[i].english; |
| |
| return xasprintf ("Language %s", language); |
| } |
| |
| |
| /* Construct the value for the PACKAGE name. */ |
| static const char * |
| project_id (const char *header) |
| { |
| const char *old_field; |
| |
| /* Return the first part of the Project-Id-Version field if present, assuming |
| it was already filled in by xgettext. */ |
| old_field = get_field (header, "Project-Id-Version"); |
| if (old_field != NULL && strcmp (old_field, "PACKAGE VERSION") != 0) |
| { |
| /* Remove the last word from old_field. */ |
| const char *last_space; |
| |
| last_space = strrchr (old_field, ' '); |
| if (last_space != NULL) |
| { |
| while (last_space > old_field && last_space[-1] == ' ') |
| last_space--; |
| if (last_space > old_field) |
| { |
| size_t package_len = last_space - old_field; |
| char *package = XNMALLOC (package_len + 1, char); |
| memcpy (package, old_field, package_len); |
| package[package_len] = '\0'; |
| |
| return package; |
| } |
| } |
| /* It contains no version, just a package name. */ |
| return old_field; |
| } |
| |
| /* On native Windows, a Bourne shell is generally not available. |
| Avoid error messages such as |
| "msginit.exe: subprocess ... failed: No such file or directory" */ |
| #if !(defined _WIN32 && ! defined __CYGWIN__) |
| { |
| const char *gettextlibdir; |
| char *prog; |
| const char *argv[3]; |
| pid_t child; |
| int fd[1]; |
| FILE *fp; |
| char *line; |
| size_t linesize; |
| size_t linelen; |
| int exitstatus; |
| |
| gettextlibdir = getenv ("GETTEXTLIBDIR_SRCDIR"); |
| if (gettextlibdir == NULL || gettextlibdir[0] == '\0') |
| gettextlibdir = relocate (LIBDIR "/gettext"); |
| |
| prog = xconcatenated_filename (gettextlibdir, "project-id", NULL); |
| |
| /* Call the project-id shell script. */ |
| argv[0] = BOURNE_SHELL; |
| argv[1] = prog; |
| argv[2] = NULL; |
| child = create_pipe_in (prog, BOURNE_SHELL, argv, NULL, |
| DEV_NULL, false, true, false, fd); |
| if (child == -1) |
| goto failed; |
| |
| /* Retrieve its result. */ |
| fp = fdopen (fd[0], "r"); |
| if (fp == NULL) |
| { |
| error (0, errno, _("fdopen() failed")); |
| goto failed; |
| } |
| |
| line = NULL; linesize = 0; |
| linelen = getline (&line, &linesize, fp); |
| if (linelen == (size_t)(-1)) |
| { |
| error (0, 0, _("%s subprocess I/O error"), prog); |
| fclose (fp); |
| goto failed; |
| } |
| if (linelen > 0 && line[linelen - 1] == '\n') |
| line[linelen - 1] = '\0'; |
| |
| fclose (fp); |
| |
| /* Remove zombie process from process list, and retrieve exit status. */ |
| exitstatus = wait_subprocess (child, prog, false, false, true, false, NULL); |
| if (exitstatus != 0) |
| { |
| error (0, 0, _("%s subprocess failed with exit code %d"), |
| prog, exitstatus); |
| goto failed; |
| } |
| |
| return line; |
| } |
| |
| failed: |
| #endif |
| return "PACKAGE"; |
| } |
| |
| |
| /* Construct the value for the Project-Id-Version field. */ |
| static const char * |
| project_id_version (const char *header) |
| { |
| const char *old_field; |
| |
| /* Return the old value if present, assuming it was already filled in by |
| xgettext. */ |
| old_field = get_field (header, "Project-Id-Version"); |
| if (old_field != NULL && strcmp (old_field, "PACKAGE VERSION") != 0) |
| return old_field; |
| |
| /* On native Windows, a Bourne shell is generally not available. |
| Avoid error messages such as |
| "msginit.exe: subprocess ... failed: No such file or directory" */ |
| #if !(defined _WIN32 && ! defined __CYGWIN__) |
| { |
| const char *gettextlibdir; |
| char *prog; |
| const char *argv[4]; |
| pid_t child; |
| int fd[1]; |
| FILE *fp; |
| char *line; |
| size_t linesize; |
| size_t linelen; |
| int exitstatus; |
| |
| gettextlibdir = getenv ("GETTEXTLIBDIR_SRCDIR"); |
| if (gettextlibdir == NULL || gettextlibdir[0] == '\0') |
| gettextlibdir = relocate (LIBDIR "/gettext"); |
| |
| prog = xconcatenated_filename (gettextlibdir, "project-id", NULL); |
| |
| /* Call the project-id shell script. */ |
| argv[0] = BOURNE_SHELL; |
| argv[1] = prog; |
| argv[2] = "yes"; |
| argv[3] = NULL; |
| child = create_pipe_in (prog, BOURNE_SHELL, argv, NULL, |
| DEV_NULL, false, true, false, fd); |
| if (child == -1) |
| goto failed; |
| |
| /* Retrieve its result. */ |
| fp = fdopen (fd[0], "r"); |
| if (fp == NULL) |
| { |
| error (0, errno, _("fdopen() failed")); |
| goto failed; |
| } |
| |
| line = NULL; linesize = 0; |
| linelen = getline (&line, &linesize, fp); |
| if (linelen == (size_t)(-1)) |
| { |
| error (0, 0, _("%s subprocess I/O error"), prog); |
| fclose (fp); |
| goto failed; |
| } |
| if (linelen > 0 && line[linelen - 1] == '\n') |
| line[linelen - 1] = '\0'; |
| |
| fclose (fp); |
| |
| /* Remove zombie process from process list, and retrieve exit status. */ |
| exitstatus = wait_subprocess (child, prog, false, false, true, false, NULL); |
| if (exitstatus != 0) |
| { |
| error (0, 0, _("%s subprocess failed with exit code %d"), |
| prog, exitstatus); |
| goto failed; |
| } |
| |
| return line; |
| } |
| |
| failed: |
| #endif |
| return "PACKAGE VERSION"; |
| } |
| |
| |
| /* Construct the value for the PO-Revision-Date field. */ |
| static const char * |
| po_revision_date (const char *header) |
| { |
| if (no_translator) |
| /* Because the PO file is automatically generated, we use the |
| POT-Creation-Date, not the current time. */ |
| return get_field (header, "POT-Creation-Date"); |
| else |
| { |
| /* Assume the translator will modify the PO file now. */ |
| time_t now; |
| |
| time (&now); |
| return po_strftime (&now); |
| } |
| } |
| |
| |
| #if HAVE_PWD_H /* Only Unix, not native Windows. */ |
| |
| /* Returns the struct passwd entry for the current user. */ |
| static struct passwd * |
| get_user_pwd () |
| { |
| const char *username; |
| struct passwd *userpasswd; |
| |
| /* 1. attempt: getpwnam(getenv("USER")) */ |
| username = getenv ("USER"); |
| if (username != NULL) |
| { |
| errno = 0; |
| userpasswd = getpwnam (username); |
| if (userpasswd != NULL) |
| return userpasswd; |
| if (errno != 0) |
| error (EXIT_FAILURE, errno, "getpwnam(\"%s\")", username); |
| } |
| |
| /* 2. attempt: getpwnam(getlogin()) */ |
| username = getlogin (); |
| if (username != NULL) |
| { |
| errno = 0; |
| userpasswd = getpwnam (username); |
| if (userpasswd != NULL) |
| return userpasswd; |
| if (errno != 0) |
| error (EXIT_FAILURE, errno, "getpwnam(\"%s\")", username); |
| } |
| |
| /* 3. attempt: getpwuid(getuid()) */ |
| errno = 0; |
| userpasswd = getpwuid (getuid ()); |
| if (userpasswd != NULL) |
| return userpasswd; |
| if (errno != 0) |
| error (EXIT_FAILURE, errno, "getpwuid(%ju)", (uintmax_t) getuid ()); |
| |
| return NULL; |
| } |
| |
| #endif |
| |
| |
| /* Return the user's full name. */ |
| static const char * |
| get_user_fullname () |
| { |
| #if HAVE_PWD_H |
| struct passwd *pwd; |
| |
| pwd = get_user_pwd (); |
| if (pwd != NULL) |
| { |
| const char *fullname; |
| const char *fullname_end; |
| char *result; |
| |
| /* Return the pw_gecos field, up to the first comma (if any). */ |
| fullname = pwd->pw_gecos; |
| fullname_end = strchr (fullname, ','); |
| if (fullname_end == NULL) |
| fullname_end = fullname + strlen (fullname); |
| |
| result = XNMALLOC (fullname_end - fullname + 1, char); |
| memcpy (result, fullname, fullname_end - fullname); |
| result[fullname_end - fullname] = '\0'; |
| |
| return result; |
| } |
| #endif |
| |
| return NULL; |
| } |
| |
| |
| /* Return the user's email address. */ |
| static const char * |
| get_user_email () |
| { |
| /* On native Windows, a Bourne shell is generally not available. |
| Avoid error messages such as |
| "msginit.exe: subprocess ... failed: No such file or directory" */ |
| #if !(defined _WIN32 && ! defined __CYGWIN__) |
| { |
| const char *prog = relocate (LIBDIR "/gettext/user-email"); |
| const char *argv[4]; |
| pid_t child; |
| int fd[1]; |
| FILE *fp; |
| char *line; |
| size_t linesize; |
| size_t linelen; |
| int exitstatus; |
| |
| /* Ask the user for his email address. */ |
| argv[0] = BOURNE_SHELL; |
| argv[1] = prog; |
| argv[2] = _("\ |
| The new message catalog should contain your email address, so that users can\n\ |
| give you feedback about the translations, and so that maintainers can contact\n\ |
| you in case of unexpected technical problems.\n"); |
| argv[3] = NULL; |
| child = create_pipe_in (prog, BOURNE_SHELL, argv, NULL, |
| DEV_NULL, false, true, false, fd); |
| if (child == -1) |
| goto failed; |
| |
| /* Retrieve his answer. */ |
| fp = fdopen (fd[0], "r"); |
| if (fp == NULL) |
| { |
| error (0, errno, _("fdopen() failed")); |
| goto failed; |
| } |
| |
| line = NULL; linesize = 0; |
| linelen = getline (&line, &linesize, fp); |
| if (linelen == (size_t)(-1)) |
| { |
| error (0, 0, _("%s subprocess I/O error"), prog); |
| fclose (fp); |
| goto failed; |
| } |
| if (linelen > 0 && line[linelen - 1] == '\n') |
| line[linelen - 1] = '\0'; |
| |
| fclose (fp); |
| |
| /* Remove zombie process from process list, and retrieve exit status. */ |
| exitstatus = wait_subprocess (child, prog, false, false, true, false, NULL); |
| if (exitstatus != 0) |
| { |
| error (0, 0, _("%s subprocess failed with exit code %d"), |
| prog, exitstatus); |
| goto failed; |
| } |
| |
| return line; |
| } |
| |
| failed: |
| #endif |
| return "EMAIL@ADDRESS"; |
| } |
| |
| |
| /* Construct the value for the Last-Translator field. */ |
| static const char * |
| last_translator () |
| { |
| if (no_translator) |
| return "Automatically generated"; |
| else |
| { |
| const char *fullname = get_user_fullname (); |
| const char *email = get_user_email (); |
| |
| if (fullname != NULL) |
| return xasprintf ("%s <%s>", fullname, email); |
| else |
| return xasprintf ("<%s>", email); |
| } |
| } |
| |
| |
| /* Return the name of the language used by the language team, in English. */ |
| static const char * |
| language_team_englishname () |
| { |
| size_t i; |
| |
| /* Search for a name depending on the catalogname. */ |
| for (i = 0; i < language_variant_table_size; i++) |
| if (strcmp (language_variant_table[i].code, catalogname) == 0) |
| return language_variant_table[i].english; |
| |
| /* Search for a name depending on the language only. */ |
| return englishname_of_language (); |
| } |
| |
| |
| /* Return the language team's mailing list address or homepage URL. */ |
| static const char * |
| language_team_address () |
| { |
| /* On native Windows, a Bourne shell is generally not available. |
| Avoid error messages such as |
| "msginit.exe: subprocess ... failed: No such file or directory" */ |
| #if !(defined _WIN32 && ! defined __CYGWIN__) |
| { |
| const char *prog = relocate (PROJECTSDIR "/team-address"); |
| const char *argv[7]; |
| pid_t child; |
| int fd[1]; |
| FILE *fp; |
| char *line; |
| size_t linesize; |
| size_t linelen; |
| int exitstatus; |
| |
| /* Call the team-address shell script. */ |
| argv[0] = BOURNE_SHELL; |
| argv[1] = prog; |
| argv[2] = relocate (PROJECTSDIR); |
| argv[3] = relocate (LIBDIR "/gettext"); |
| argv[4] = catalogname; |
| argv[5] = language; |
| argv[6] = NULL; |
| child = create_pipe_in (prog, BOURNE_SHELL, argv, NULL, |
| DEV_NULL, false, true, false, fd); |
| if (child == -1) |
| goto failed; |
| |
| /* Retrieve its result. */ |
| fp = fdopen (fd[0], "r"); |
| if (fp == NULL) |
| { |
| error (0, errno, _("fdopen() failed")); |
| goto failed; |
| } |
| |
| line = NULL; linesize = 0; |
| linelen = getline (&line, &linesize, fp); |
| if (linelen == (size_t)(-1)) |
| line = ""; |
| else if (linelen > 0 && line[linelen - 1] == '\n') |
| line[linelen - 1] = '\0'; |
| |
| fclose (fp); |
| |
| /* Remove zombie process from process list, and retrieve exit status. */ |
| exitstatus = wait_subprocess (child, prog, false, false, true, false, NULL); |
| if (exitstatus != 0) |
| { |
| error (0, 0, _("%s subprocess failed with exit code %d"), |
| prog, exitstatus); |
| goto failed; |
| } |
| |
| return line; |
| } |
| |
| failed: |
| #endif |
| return ""; |
| } |
| |
| |
| /* Construct the value for the Language-Team field. */ |
| static const char * |
| language_team () |
| { |
| if (no_translator) |
| return "none"; |
| else |
| { |
| const char *englishname = language_team_englishname (); |
| const char *address = language_team_address (); |
| |
| if (address != NULL && address[0] != '\0') |
| return xasprintf ("%s %s", englishname, address); |
| else |
| return englishname; |
| } |
| } |
| |
| |
| /* Construct the value for the Language field. */ |
| static const char * |
| language_value () |
| { |
| return catalogname; |
| } |
| |
| |
| /* Construct the value for the MIME-Version field. */ |
| static const char * |
| mime_version () |
| { |
| return "1.0"; |
| } |
| |
| |
| /* Construct the value for the Content-Type field. */ |
| static const char * |
| content_type (const char *header) |
| { |
| bool was_utf8; |
| const char *old_field; |
| |
| /* If the POT file contains charset=UTF-8, it means that the POT file |
| contains non-ASCII characters, and we keep the UTF-8 encoding. |
| Otherwise, when the POT file is plain ASCII, we use the locale's |
| encoding. */ |
| was_utf8 = false; |
| old_field = get_field (header, "Content-Type"); |
| if (old_field != NULL) |
| { |
| const char *charsetstr = c_strstr (old_field, "charset="); |
| |
| if (charsetstr != NULL) |
| { |
| charsetstr += strlen ("charset="); |
| was_utf8 = (c_strcasecmp (charsetstr, "UTF-8") == 0); |
| } |
| } |
| return xasprintf ("text/plain; charset=%s", |
| was_utf8 ? "UTF-8" : canonical_locale_charset ()); |
| } |
| |
| |
| /* Construct the value for the Content-Transfer-Encoding field. */ |
| static const char * |
| content_transfer_encoding () |
| { |
| return "8bit"; |
| } |
| |
| |
| /* Construct the value for the Plural-Forms field. */ |
| static const char * |
| plural_forms () |
| { |
| const char *gettextcldrdir; |
| char *prog = NULL; |
| size_t i; |
| |
| /* Search for a formula depending on the catalogname. */ |
| for (i = 0; i < plural_table_size; i++) |
| if (strcmp (plural_table[i].lang, catalogname) == 0) |
| return plural_table[i].value; |
| |
| /* Search for a formula depending on the language only. */ |
| for (i = 0; i < plural_table_size; i++) |
| if (strcmp (plural_table[i].lang, language) == 0) |
| return plural_table[i].value; |
| |
| gettextcldrdir = getenv ("GETTEXTCLDRDIR"); |
| if (gettextcldrdir != NULL && gettextcldrdir[0] != '\0') |
| { |
| const char *gettextlibdir; |
| char *dirs[3], *last_dir; |
| const char *argv[4]; |
| pid_t child; |
| int fd[1]; |
| FILE *fp; |
| char *line; |
| size_t linesize; |
| size_t linelen; |
| int exitstatus; |
| |
| gettextlibdir = getenv ("GETTEXTLIBDIR_BUILDDIR"); |
| if (gettextlibdir == NULL || gettextlibdir[0] == '\0') |
| gettextlibdir = relocate (LIBDIR "/gettext"); |
| |
| prog = xconcatenated_filename (gettextlibdir, "cldr-plurals", EXEEXT); |
| |
| last_dir = xstrdup (gettextcldrdir); |
| dirs[0] = "common"; |
| dirs[1] = "supplemental"; |
| dirs[2] = "plurals.xml"; |
| for (i = 0; i < SIZEOF (dirs); i++) |
| { |
| char *dir = xconcatenated_filename (last_dir, dirs[i], NULL); |
| free (last_dir); |
| last_dir = dir; |
| } |
| |
| /* Call the cldr-plurals command. |
| argv[0] must be prog, not just the base name "cldr-plurals", |
| because on Cygwin in a build with --enable-shared, the libtool |
| wrapper of cldr-plurals.exe apparently needs this. */ |
| argv[0] = prog; |
| argv[1] = language; |
| argv[2] = last_dir; |
| argv[3] = NULL; |
| child = create_pipe_in (prog, prog, argv, NULL, |
| DEV_NULL, false, true, false, fd); |
| free (last_dir); |
| if (child == -1) |
| goto failed; |
| |
| /* Retrieve its result. */ |
| fp = fdopen (fd[0], "r"); |
| if (fp == NULL) |
| { |
| error (0, errno, _("fdopen() failed")); |
| goto failed; |
| } |
| |
| line = NULL; linesize = 0; |
| linelen = getline (&line, &linesize, fp); |
| if (linelen == (size_t)(-1)) |
| { |
| error (0, 0, _("%s subprocess I/O error"), prog); |
| fclose (fp); |
| goto failed; |
| } |
| if (linelen > 0 && line[linelen - 1] == '\n') |
| { |
| line[linelen - 1] = '\0'; |
| #if defined _WIN32 && ! defined __CYGWIN__ |
| if (linelen > 1 && line[linelen - 2] == '\r') |
| line[linelen - 2] = '\0'; |
| #endif |
| } |
| |
| fclose (fp); |
| |
| /* Remove zombie process from process list, and retrieve exit status. */ |
| exitstatus = wait_subprocess (child, prog, false, false, true, false, |
| NULL); |
| if (exitstatus != 0) |
| { |
| error (0, 0, _("%s subprocess failed with exit code %d"), |
| prog, exitstatus); |
| goto failed; |
| } |
| |
| return line; |
| } |
| |
| failed: |
| free (prog); |
| return NULL; |
| } |
| |
| |
| static struct |
| { |
| const char *name; |
| const char * (*getter0) (void); |
| const char * (*getter1) (const char *header); |
| } |
| fields[] = |
| { |
| { "Project-Id-Version", NULL, project_id_version }, |
| { "PO-Revision-Date", NULL, po_revision_date }, |
| { "Last-Translator", last_translator, NULL }, |
| { "Language-Team", language_team, NULL }, |
| { "Language", language_value, NULL }, |
| { "MIME-Version", mime_version, NULL }, |
| { "Content-Type", NULL, content_type }, |
| { "Content-Transfer-Encoding", content_transfer_encoding, NULL }, |
| { "Plural-Forms", plural_forms, NULL } |
| }; |
| |
| #define NFIELDS SIZEOF (fields) |
| #define FIELD_LAST_TRANSLATOR 2 |
| |
| |
| /* Retrieve a freshly allocated copy of a field's value. */ |
| static char * |
| get_field (const char *header, const char *field) |
| { |
| size_t len = strlen (field); |
| const char *line; |
| |
| for (line = header;;) |
| { |
| if (strncmp (line, field, len) == 0 && line[len] == ':') |
| { |
| const char *value_start; |
| const char *value_end; |
| char *value; |
| |
| value_start = line + len + 1; |
| if (*value_start == ' ') |
| value_start++; |
| value_end = strchr (value_start, '\n'); |
| if (value_end == NULL) |
| value_end = value_start + strlen (value_start); |
| |
| value = XNMALLOC (value_end - value_start + 1, char); |
| memcpy (value, value_start, value_end - value_start); |
| value[value_end - value_start] = '\0'; |
| |
| return value; |
| } |
| |
| line = strchr (line, '\n'); |
| if (line != NULL) |
| line++; |
| else |
| break; |
| } |
| |
| return NULL; |
| } |
| |
| /* Add a field with value to a header, and return the new header. */ |
| static char * |
| put_field (const char *old_header, const char *field, const char *value) |
| { |
| size_t len = strlen (field); |
| const char *line; |
| char *new_header; |
| char *p; |
| |
| for (line = old_header;;) |
| { |
| if (strncmp (line, field, len) == 0 && line[len] == ':') |
| { |
| const char *value_start; |
| const char *value_end; |
| |
| value_start = line + len + 1; |
| if (*value_start == ' ') |
| value_start++; |
| value_end = strchr (value_start, '\n'); |
| if (value_end == NULL) |
| value_end = value_start + strlen (value_start); |
| |
| new_header = XNMALLOC (strlen (old_header) |
| - (value_end - value_start) |
| + strlen (value) |
| + (*value_end != '\n' ? 1 : 0) |
| + 1, |
| char); |
| p = new_header; |
| memcpy (p, old_header, value_start - old_header); |
| p += value_start - old_header; |
| memcpy (p, value, strlen (value)); |
| p += strlen (value); |
| if (*value_end != '\n') |
| *p++ = '\n'; |
| strcpy (p, value_end); |
| |
| return new_header; |
| } |
| |
| line = strchr (line, '\n'); |
| if (line != NULL) |
| line++; |
| else |
| break; |
| } |
| |
| new_header = XNMALLOC (strlen (old_header) + 1 |
| + len + 2 + strlen (value) + 1 |
| + 1, |
| char); |
| p = new_header; |
| memcpy (p, old_header, strlen (old_header)); |
| p += strlen (old_header); |
| if (p > new_header && p[-1] != '\n') |
| *p++ = '\n'; |
| memcpy (p, field, len); |
| p += len; |
| *p++ = ':'; |
| *p++ = ' '; |
| memcpy (p, value, strlen (value)); |
| p += strlen (value); |
| *p++ = '\n'; |
| *p = '\0'; |
| |
| return new_header; |
| } |
| |
| |
| /* Return the title format string. */ |
| static const char * |
| get_title () |
| { |
| /* This is tricky. We want the translation in the given locale specified by |
| the command line, not the current locale. But we want it in the encoding |
| that we put into the header entry, not the encoding of that locale. |
| We could avoid the use of OUTPUT_CHARSET by using a separate message |
| catalog and bind_textdomain_codeset(), but that doesn't seem worth the |
| trouble for one single message. */ |
| const char *encoding; |
| const char *tmp; |
| char *old_LC_ALL; |
| char *old_LANGUAGE; |
| char *old_OUTPUT_CHARSET; |
| const char *msgid; |
| const char *english; |
| const char *result; |
| |
| encoding = canonical_locale_charset (); |
| |
| /* First, the English title. */ |
| english = xasprintf ("%s translations for %%s package", |
| englishname_of_language ()); |
| |
| /* Save LC_ALL, LANGUAGE, OUTPUT_CHARSET environment variables. */ |
| |
| tmp = getenv ("LC_ALL"); |
| old_LC_ALL = (tmp != NULL ? xstrdup (tmp) : NULL); |
| |
| tmp = getenv ("LANGUAGE"); |
| old_LANGUAGE = (tmp != NULL ? xstrdup (tmp) : NULL); |
| |
| tmp = getenv ("OUTPUT_CHARSET"); |
| old_OUTPUT_CHARSET = (tmp != NULL ? xstrdup (tmp) : NULL); |
| |
| xsetenv ("LC_ALL", locale, 1); |
| unsetenv ("LANGUAGE"); |
| xsetenv ("OUTPUT_CHARSET", encoding, 1); |
| |
| if (setlocale (LC_ALL, "") == NULL) |
| /* Nonexistent locale. Use the English title. */ |
| result = english; |
| else |
| { |
| /* Fetch the translation. */ |
| /* TRANSLATORS: "English" needs to be replaced by your language. |
| For example in it.po write "Traduzioni italiani ...", |
| *not* "Traduzioni inglesi ...". */ |
| msgid = N_("English translations for %s package"); |
| result = gettext (msgid); |
| if (result != msgid && strcmp (result, msgid) != 0) |
| /* Use the English and the foreign title. */ |
| result = xasprintf ("%s\n%s", english, result); |
| else |
| /* No translation found. Use the English title. */ |
| result = english; |
| } |
| |
| /* Restore LC_ALL, LANGUAGE, OUTPUT_CHARSET environment variables. */ |
| |
| if (old_LC_ALL != NULL) |
| xsetenv ("LC_ALL", old_LC_ALL, 1), free (old_LC_ALL); |
| else |
| unsetenv ("LC_ALL"); |
| |
| if (old_LANGUAGE != NULL) |
| xsetenv ("LANGUAGE", old_LANGUAGE, 1), free (old_LANGUAGE); |
| else |
| unsetenv ("LANGUAGE"); |
| |
| if (old_OUTPUT_CHARSET != NULL) |
| xsetenv ("OUTPUT_CHARSET", old_OUTPUT_CHARSET, 1), free (old_OUTPUT_CHARSET); |
| else |
| unsetenv ("OUTPUT_CHARSET"); |
| |
| setlocale (LC_ALL, ""); |
| |
| return result; |
| } |
| |
| |
| /* Perform a set of substitutions in a string and return the resulting |
| string. When subst[j][0] found, it is replaced with subst[j][1]. |
| subst[j][0] must not be the empty string. */ |
| static const char * |
| subst_string (const char *str, |
| unsigned int nsubst, const char *(*subst)[2]) |
| { |
| if (nsubst > 0) |
| { |
| char *malloced = NULL; |
| size_t *substlen; |
| size_t i; |
| unsigned int j; |
| |
| substlen = (size_t *) xmalloca (nsubst * sizeof (size_t)); |
| for (j = 0; j < nsubst; j++) |
| { |
| substlen[j] = strlen (subst[j][0]); |
| if (substlen[j] == 0) |
| abort (); |
| } |
| |
| for (i = 0;;) |
| { |
| if (str[i] == '\0') |
| break; |
| for (j = 0; j < nsubst; j++) |
| if (*(str + i) == *subst[j][0] |
| && strncmp (str + i, subst[j][0], substlen[j]) == 0) |
| { |
| size_t replacement_len = strlen (subst[j][1]); |
| size_t new_len = strlen (str) - substlen[j] + replacement_len; |
| char *new_str = XNMALLOC (new_len + 1, char); |
| memcpy (new_str, str, i); |
| memcpy (new_str + i, subst[j][1], replacement_len); |
| strcpy (new_str + i + replacement_len, str + i + substlen[j]); |
| if (malloced != NULL) |
| free (malloced); |
| str = new_str; |
| malloced = new_str; |
| i += replacement_len; |
| break; |
| } |
| if (j == nsubst) |
| i++; |
| } |
| |
| freea (substlen); |
| } |
| |
| return str; |
| } |
| |
| /* Perform a set of substitutions on each string of a string list. |
| When subst[j][0] found, it is replaced with subst[j][1]. subst[j][0] |
| must not be the empty string. */ |
| static void |
| subst_string_list (string_list_ty *slp, |
| unsigned int nsubst, const char *(*subst)[2]) |
| { |
| size_t j; |
| |
| for (j = 0; j < slp->nitems; j++) |
| slp->item[j] = subst_string (slp->item[j], nsubst, subst); |
| } |
| |
| |
| /* Fill the templates in all fields of the header entry. */ |
| static msgdomain_list_ty * |
| fill_header (msgdomain_list_ty *mdlp) |
| { |
| /* Cache the strings filled in, for use when there are multiple domains |
| and a header entry for each domain. */ |
| const char *field_value[NFIELDS]; |
| size_t k, j, i; |
| |
| for (i = 0; i < NFIELDS; i++) |
| field_value[i] = NULL; |
| |
| for (k = 0; k < mdlp->nitems; k++) |
| { |
| message_list_ty *mlp = mdlp->item[k]->messages; |
| |
| if (mlp->nitems > 0) |
| { |
| message_ty *header_mp = NULL; |
| char *header; |
| |
| /* Search the header entry. */ |
| for (j = 0; j < mlp->nitems; j++) |
| if (is_header (mlp->item[j]) && !mlp->item[j]->obsolete) |
| { |
| header_mp = mlp->item[j]; |
| break; |
| } |
| |
| /* If it wasn't found, provide one. */ |
| if (header_mp == NULL) |
| { |
| static lex_pos_ty pos = { __FILE__, __LINE__ }; |
| |
| header_mp = message_alloc (NULL, "", NULL, "", 1, &pos); |
| message_list_prepend (mlp, header_mp); |
| } |
| |
| header = xstrdup (header_mp->msgstr); |
| |
| /* Fill in the fields. */ |
| for (i = 0; i < NFIELDS; i++) |
| { |
| if (field_value[i] == NULL) |
| field_value[i] = |
| (fields[i].getter1 != NULL |
| ? fields[i].getter1 (header) |
| : fields[i].getter0 ()); |
| |
| if (field_value[i] != NULL) |
| { |
| char *old_header = header; |
| header = put_field (header, fields[i].name, field_value[i]); |
| free (old_header); |
| } |
| } |
| |
| /* Replace the old translation in the header entry. */ |
| header_mp->msgstr = header; |
| header_mp->msgstr_len = strlen (header) + 1; |
| |
| /* Update the comments in the header entry. */ |
| if (header_mp->comment != NULL) |
| { |
| const char *subst[4][2]; |
| const char *id; |
| time_t now; |
| |
| id = project_id (header); |
| subst[0][0] = "SOME DESCRIPTIVE TITLE"; |
| subst[0][1] = xasprintf (get_title (), id, id); |
| subst[1][0] = "PACKAGE"; |
| subst[1][1] = id; |
| subst[2][0] = "FIRST AUTHOR <EMAIL@ADDRESS>"; |
| subst[2][1] = field_value[FIELD_LAST_TRANSLATOR]; |
| subst[3][0] = "YEAR"; |
| subst[3][1] = |
| xasprintf ("%d", |
| (time (&now), (localtime (&now))->tm_year + 1900)); |
| subst_string_list (header_mp->comment, SIZEOF (subst), subst); |
| } |
| |
| /* Finally remove the fuzzy attribute. */ |
| header_mp->is_fuzzy = false; |
| } |
| } |
| |
| return mdlp; |
| } |
| |
| |
| /* Update the msgstr plural entries according to the nplurals count. */ |
| static msgdomain_list_ty * |
| update_msgstr_plurals (msgdomain_list_ty *mdlp) |
| { |
| size_t k; |
| |
| for (k = 0; k < mdlp->nitems; k++) |
| { |
| message_list_ty *mlp = mdlp->item[k]->messages; |
| message_ty *header_entry; |
| unsigned long int nplurals; |
| char *untranslated_plural_msgstr; |
| size_t j; |
| |
| header_entry = message_list_search (mlp, NULL, ""); |
| nplurals = get_plural_count (header_entry ? header_entry->msgstr : NULL); |
| untranslated_plural_msgstr = XNMALLOC (nplurals, char); |
| memset (untranslated_plural_msgstr, '\0', nplurals); |
| |
| for (j = 0; j < mlp->nitems; j++) |
| { |
| message_ty *mp = mlp->item[j]; |
| bool is_untranslated; |
| const char *p; |
| const char *pend; |
| |
| if (mp->msgid_plural != NULL) |
| { |
| /* Test if mp is untranslated. (It most likely is.) */ |
| is_untranslated = true; |
| for (p = mp->msgstr, pend = p + mp->msgstr_len; p < pend; p++) |
| if (*p != '\0') |
| { |
| is_untranslated = false; |
| break; |
| } |
| if (is_untranslated) |
| { |
| /* Change mp->msgstr_len consecutive empty strings into |
| nplurals consecutive empty strings. */ |
| if (nplurals > mp->msgstr_len) |
| mp->msgstr = untranslated_plural_msgstr; |
| mp->msgstr_len = nplurals; |
| } |
| } |
| } |
| } |
| return mdlp; |
| } |