aboutsummaryrefslogtreecommitdiffstats
/*
    Checks the po file for C format correctness.
    To compile it you need the gettext package installed
    with its development files.
    Compilation commandline:

      gcc -lgettextpo -o msgcheckformat msgcheckformat.c

    Usage:

      msgcheckformat <pofile> [ask|fix]

    ----------------------------------------------------------------------------

    Copyright (C) 2004 Szymon Stefanek (pragma at kvirc dot net),
                  2010 Fabio Bas (ctrlaltca at gmail dot com)

    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 2, 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.
*/



#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <gettext-po.h>

int total_errors = 0;
int total_warnings = 0;
int total_remove = 0;
int total_ignore = 0;
int total_fix = 0;
int solution_mode = 0;

static void po_xerror( int severity, po_message_t message,
                       const char *filename, size_t lineno, size_t column,
                       int multiline_p, const char *message_text )
{
    fprintf( stderr, "%s:%u:%u: %s\n",
             filename, (unsigned int)lineno, (unsigned int)column, message_text );
    if (severity) exit(1);
}

static void po_xerror2( int severity, po_message_t message1,
                        const char *filename1, size_t lineno1, size_t column1,
                        int multiline_p1, const char *message_text1,
                        po_message_t message2,
                        const char *filename2, size_t lineno2, size_t column2,
                        int multiline_p2, const char *message_text2 )
{
    fprintf( stderr, "%s:%u:%u: %s\n",
             filename1, (unsigned int)lineno1, (unsigned int)column1, message_text1 );
    fprintf( stderr, "%s:%u:%u: %s\n",
             filename2, (unsigned int)lineno2, (unsigned int)column2, message_text2 );
    if (severity) exit(1);
}

static const struct po_xerror_handler po_xerror_handler = { po_xerror, po_xerror2 };

static void replace_crlf(const char * msg,char * buf)
{
	while(*msg)
	{
		if(*msg == '\r')
		{
			*buf++ = '\\';
			*buf++ = 'r';
			msg++;
		} else if(*msg == '\n')
		{
			*buf++ = '\\';
			*buf++ = 'n';
			msg++;
		} else *buf++ = *msg++;
	}
}


static int print_error(po_message_t m,const char * msgid,const char * msgstr,const char * error, int solution)
{
	char bufferid[16000];
	char bufferstr[16000];
	int s = 0;
	int is_fuzzy = po_message_is_fuzzy(m) ? 1 : 0;
	replace_crlf(msgid,bufferid);
	replace_crlf(msgstr,bufferstr);
	fprintf(stderr,"%s in %s:\n msgid: %s\n msgstr: %s\n reason: %s\n\n",
		is_fuzzy ? "Warning" : "Error",
		is_fuzzy ? "fuzzy entry" : "entry",
		bufferid,bufferstr,error);
	if(is_fuzzy)total_warnings++;
	else total_errors++;
	switch(solution_mode)
	{
		case 1:
			// ask the user
			switch(solution)
			{
				case 1:
					printf("Pick a solution: (i)gnore the problem, or (r)emove the offending translation:");
					while(s != 'i' && s != 'r') s = getchar();
					if(s == 'r')
					{
						// delete the wrong translation
						/* Change the msgstr (translation) of a message.
							Use an empty string to denote an untranslated message.  */
						po_message_set_msgstr(m, "");
						total_remove++;
					} else {
						total_ignore++;
					}
					break;
				case 2:
					printf("Pick a solution: (i)gnore the problem, (f)ix it or (r)emove the offending translation:");
					while(s != 'i' && s != 'f' && s != 'r') s = getchar();
					if(s == 'r')
					{
						// delete the wrong translation
						/* Change the msgstr (translation) of a message.
							Use an empty string to denote an untranslated message.  */
						po_message_set_msgstr(m, "");
						total_remove++;
					} else if(s == 'f') {
						total_fix++;
						return 1;
					} else {
						total_ignore++;
					}
					break;
			}
			break;
		case 2:
			// fix automatically
			switch(solution)
			{
				case 1:
					// delete the wrong translation
					/* Change the msgstr (translation) of a message.
						Use an empty string to denote an untranslated message.  */
					po_message_set_msgstr(m, "");
					total_remove++;
					break;
				case 2:
					total_fix++;
					return 1;
					break;
			}
			break;
		default:
			// do nothing
			break;
	}
	return 0;
}

inline int is_valid_fmtstr(const char * p)
{
	if(!*p) return 0;
	if(*p != '%') return 0;
	p++;
	if(!*p) return 0;
	if((*p >= '1' && *p <= '9') ||
		(*p >= 'a' && *p <= 'z') ||
		(*p >= 'A' && *p <= 'Z'))
	{
		p--;
		return 1;
	}
	p--;
	return 0;
}

static void process_message(po_message_t m)
{
	const char * msgstr = po_message_msgstr(m);
	const char * msgid = po_message_msgid(m);
	const char *p;
	char *q;
	char *fixedstr;

	if(msgid[0] == '\0')return;
	if(strlen(msgid) < 1)return;
	if(strlen(msgstr) < 1)return;
	if(msgstr[0] == '\0')return;

	fixedstr = strdup(msgstr);

	p = msgid;
	q = fixedstr;

	while(*p && *q)
	{
		while(*p && !is_valid_fmtstr(p))p++;
		while(*q && !is_valid_fmtstr(q))q++;
		if(*q && *p)
		{
			q++;
			p++;
			if(*p != *q)
			{
				char buff[200];
				sprintf(buff,"Mismatched format character (%c != %c)",*p,*q);
				if(print_error(m,msgid,fixedstr,buff, 2))
				{
					//fix the format string
					*q=*p;
					po_message_set_msgstr(m, fixedstr);
				}
			}
		}
	}

	if(*p || *q)
	{
		print_error(m,msgid,msgstr,"Mismatched number of format characters", 1);
	}
}

static void process_messages(po_message_iterator_t it)
{
	po_message_t m = po_next_message(it);
	while(m)
	{
		process_message(m);
		m = po_next_message(it);
	}
}

int main(int argc, char **argv)
{
	po_file_t po;
	po_message_iterator_t it;
	const char * const * domains;

	if(!argv[1])
	{
		fprintf(stderr,"usage: %s <pofile> [ask|fix]\n",argv[0]);
		return 0;
	}

	if(argv[2])
	{
		if(!strncmp(argv[2], "ask", 3))
		{
			solution_mode=1;
		} else if(!strncmp(argv[2], "fix", 3)) {
			solution_mode=2;
		} else {
			fprintf(stderr,"usage: %s <pofile> [ask|fix]\n",argv[0]);
			return 0;
		}
	}

	po = po_file_read(argv[1], &po_xerror_handler);

	if(!po)
	{
		fprintf(stderr,"Couldn't read the input po file\n");
		return 0;
	}

	domains = po_file_domains(po);

	if(!domains)
	{
		fprintf(stderr,"Couldn't find the message domains in the po file\n");
		return 0;
	}

	while(*domains)
	{
		it = po_message_iterator(po,*domains);
		process_messages(it);
		po_message_iterator_free(it);
		domains++;
	}

	if(total_errors == 0 && total_warnings == 0)
	{
		fprintf(stderr,"No errors found\n");
	} else if(solution_mode==0) {
		fprintf(stderr,"%d warnings, %d errors\n",total_warnings,total_errors);
	} else {
		fprintf(stderr,"%d warnings, %d errors, %d fixes, %d removed, %d ignored\n",total_warnings,total_errors, total_fix, total_remove, total_ignore);
		if(total_fix || total_remove)
		{
			int s = 0;
			printf("Commit changes to file? (y)es, (n)o:");
			while(s != 'y' && s != 'n') s = getchar();
			if(s == 'y')
			{
				if(po_file_write(po, argv[1], &po_xerror_handler)) printf("Changes committed.");
				else printf("Error writing file.");
			} else {
				printf("Changes discarded.");
			}
		}
	}

	po_file_free(po);

	return 0;
}