2000-11-11 10:39:27 +01:00
|
|
|
/*
|
|
|
|
* i18n.c: Internationalization
|
|
|
|
*
|
|
|
|
* See the main source file 'vdr.c' for copyright information and
|
|
|
|
* how to reach the author.
|
|
|
|
*
|
2021-05-21 09:50:57 +02:00
|
|
|
* $Id: i18n.c 5.1 2021/05/21 09:50:57 kls Exp $
|
2000-11-11 10:39:27 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
2002-03-08 16:11:34 +01:00
|
|
|
* In case an English phrase is used in more than one context (and might need
|
2005-09-09 15:14:16 +02:00
|
|
|
* different translations in other languages) it can be preceded with an
|
2002-03-08 16:11:34 +01:00
|
|
|
* arbitrary string to describe its context, separated from the actual phrase
|
|
|
|
* by a '$' character (see for instance "Button$Stop" vs. "Stop").
|
|
|
|
* Of course this means that no English phrase may contain the '$' character!
|
|
|
|
* If this should ever become necessary, the existing '$' would have to be
|
|
|
|
* replaced with something different...
|
2000-11-11 10:39:27 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "i18n.h"
|
2004-11-14 14:28:29 +01:00
|
|
|
#include <ctype.h>
|
2007-08-11 12:39:06 +02:00
|
|
|
#include <libintl.h>
|
|
|
|
#include <locale.h>
|
2007-08-19 14:34:44 +02:00
|
|
|
#include <unistd.h>
|
2000-11-11 10:39:27 +01:00
|
|
|
#include "tools.h"
|
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
// TRANSLATORS: The name of the language, as written natively
|
|
|
|
const char *LanguageName = trNOOP("LanguageName$English");
|
2007-08-12 09:54:23 +02:00
|
|
|
// TRANSLATORS: The 3-letter code of the language
|
|
|
|
const char *LanguageCode = trNOOP("LanguageCode$eng");
|
|
|
|
|
|
|
|
// List of known language codes with aliases.
|
|
|
|
// Actually we could list all codes from http://www.loc.gov/standards/iso639-2
|
|
|
|
// here, but that would be several hundreds - and for most of them it's unlikely
|
|
|
|
// they're ever going to be used...
|
|
|
|
|
|
|
|
const char *LanguageCodeList[] = {
|
|
|
|
"eng,dos",
|
|
|
|
"deu,ger",
|
2020-06-15 15:57:32 +02:00
|
|
|
"alb,sqi",
|
|
|
|
"ara",
|
|
|
|
"bos",
|
|
|
|
"bul",
|
|
|
|
"cat,cln",
|
|
|
|
"chi,zho",
|
|
|
|
"cze,ces",
|
|
|
|
"dan",
|
2007-08-12 09:54:23 +02:00
|
|
|
"dut,nla,nld",
|
2020-06-15 15:57:32 +02:00
|
|
|
"ell,gre",
|
|
|
|
"esl,spa",
|
|
|
|
"est",
|
|
|
|
"eus,baq",
|
|
|
|
"fin,suo",
|
2007-08-12 09:54:23 +02:00
|
|
|
"fra,fre",
|
2020-06-15 15:57:32 +02:00
|
|
|
"hrv",
|
|
|
|
"hun",
|
|
|
|
"iri,gle", // 'NorDig'
|
|
|
|
"ita",
|
|
|
|
"jpn",
|
|
|
|
"lav",
|
|
|
|
"lit",
|
|
|
|
"ltz",
|
|
|
|
"mac,mkd",
|
|
|
|
"mlt",
|
2007-08-12 09:54:23 +02:00
|
|
|
"nor",
|
|
|
|
"pol",
|
2020-06-15 15:57:32 +02:00
|
|
|
"por",
|
2007-08-12 09:54:23 +02:00
|
|
|
"rom,rum",
|
|
|
|
"rus",
|
2020-06-15 15:57:32 +02:00
|
|
|
"slk,slo",
|
|
|
|
"slv",
|
|
|
|
"smi", // 'NorDig' Sami language (Norway, Sweden, Finnland, Russia)
|
2011-02-20 16:08:39 +01:00
|
|
|
"srb,srp,scr,scc",
|
2020-06-15 15:57:32 +02:00
|
|
|
"sve,swe",
|
2007-08-12 09:54:23 +02:00
|
|
|
"tur",
|
2007-11-01 10:38:27 +01:00
|
|
|
"ukr",
|
2007-08-12 09:54:23 +02:00
|
|
|
NULL
|
|
|
|
};
|
2000-11-11 10:39:27 +01:00
|
|
|
|
2020-06-15 15:57:32 +02:00
|
|
|
struct tSpecialLc { const char *Code; const char *Name; };
|
|
|
|
const struct tSpecialLc SpecialLanguageCodeList[] = {
|
|
|
|
{ "qaa", trNOOP("LanguageName$original language (qaa)") },
|
2021-05-21 09:50:57 +02:00
|
|
|
{ "qad", trNOOP("LanguageName$audio description (qad)") },
|
2020-06-15 15:57:32 +02:00
|
|
|
{ "mis", trNOOP("LanguageName$uncoded languages (mis)") },
|
|
|
|
{ "mul", trNOOP("LanguageName$multiple languages (mul)") },
|
|
|
|
{ "nar", trNOOP("LanguageName$narrative (nar)") },
|
|
|
|
{ "und", trNOOP("LanguageName$undetermined (und)") },
|
|
|
|
{ "zxx", trNOOP("LanguageName$no linguistic content (zxx)") },
|
|
|
|
{ NULL, NULL }
|
|
|
|
};
|
|
|
|
|
2012-09-01 14:03:45 +02:00
|
|
|
static cString I18nLocaleDir;
|
2002-05-09 16:26:56 +02:00
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
static cStringList LanguageLocales;
|
|
|
|
static cStringList LanguageNames;
|
|
|
|
static cStringList LanguageCodes;
|
2002-05-09 16:26:56 +02:00
|
|
|
|
2007-08-18 09:21:52 +02:00
|
|
|
static int NumLocales = 1;
|
2020-06-15 15:57:32 +02:00
|
|
|
static int NumLanguages = 1;
|
2007-08-11 12:39:06 +02:00
|
|
|
static int CurrentLanguage = 0;
|
2002-05-09 16:26:56 +02:00
|
|
|
|
2007-08-12 09:54:23 +02:00
|
|
|
static bool ContainsCode(const char *Codes, const char *Code)
|
|
|
|
{
|
|
|
|
while (*Codes) {
|
|
|
|
int l = 0;
|
|
|
|
for ( ; l < 3 && Code[l]; l++) {
|
|
|
|
if (Codes[l] != tolower(Code[l]))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (l == 3)
|
|
|
|
return true;
|
|
|
|
Codes++;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
static const char *SkipContext(const char *s)
|
2007-06-10 13:02:43 +02:00
|
|
|
{
|
2007-08-11 12:39:06 +02:00
|
|
|
const char *p = strchr(s, '$');
|
|
|
|
return p ? p + 1 : s;
|
2007-06-10 13:02:43 +02:00
|
|
|
}
|
|
|
|
|
2007-08-19 14:34:44 +02:00
|
|
|
static void SetEnvLanguage(const char *Locale)
|
|
|
|
{
|
|
|
|
setenv("LANGUAGE", Locale, 1);
|
|
|
|
extern int _nl_msg_cat_cntr;
|
|
|
|
++_nl_msg_cat_cntr;
|
|
|
|
}
|
|
|
|
|
2020-06-15 15:57:32 +02:00
|
|
|
static void SetLanguageNames(void)
|
|
|
|
{
|
|
|
|
// Update the translation for special language codes:
|
|
|
|
int i = NumLanguages;
|
|
|
|
for (const struct tSpecialLc *slc = SpecialLanguageCodeList; slc->Code; slc++) {
|
|
|
|
const char *TranslatedName = gettext(slc->Name);
|
|
|
|
free(LanguageNames[i]);
|
|
|
|
LanguageNames[i++] = strdup(TranslatedName != slc->Name ? TranslatedName : SkipContext(slc->Name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-01-19 12:25:54 +01:00
|
|
|
void I18nInitialize(const char *LocaleDir)
|
2002-05-09 16:26:56 +02:00
|
|
|
{
|
2012-09-01 14:03:45 +02:00
|
|
|
I18nLocaleDir = LocaleDir;
|
2007-08-11 12:39:06 +02:00
|
|
|
LanguageLocales.Append(strdup(I18N_DEFAULT_LOCALE));
|
2007-08-12 09:54:23 +02:00
|
|
|
LanguageNames.Append(strdup(SkipContext(LanguageName)));
|
|
|
|
LanguageCodes.Append(strdup(LanguageCodeList[0]));
|
2007-08-11 12:39:06 +02:00
|
|
|
textdomain("vdr");
|
|
|
|
bindtextdomain("vdr", I18nLocaleDir);
|
|
|
|
cFileNameList Locales(I18nLocaleDir, true);
|
|
|
|
if (Locales.Size() > 0) {
|
|
|
|
char *OldLocale = strdup(setlocale(LC_MESSAGES, NULL));
|
|
|
|
for (int i = 0; i < Locales.Size(); i++) {
|
2012-09-01 14:03:45 +02:00
|
|
|
cString FileName = cString::sprintf("%s/%s/LC_MESSAGES/vdr.mo", *I18nLocaleDir, Locales[i]);
|
2007-08-19 14:34:44 +02:00
|
|
|
if (access(FileName, F_OK) == 0) { // found a locale with VDR texts
|
2007-08-19 16:04:22 +02:00
|
|
|
if (NumLocales < I18N_MAX_LANGUAGES - 1) {
|
2007-08-19 14:34:44 +02:00
|
|
|
SetEnvLanguage(Locales[i]);
|
2007-08-24 13:09:09 +02:00
|
|
|
const char *TranslatedLanguageName = gettext(LanguageName);
|
|
|
|
if (TranslatedLanguageName != LanguageName) {
|
|
|
|
NumLocales++;
|
|
|
|
if (strstr(OldLocale, Locales[i]) == OldLocale)
|
|
|
|
CurrentLanguage = LanguageLocales.Size();
|
|
|
|
LanguageLocales.Append(strdup(Locales[i]));
|
|
|
|
LanguageNames.Append(strdup(TranslatedLanguageName));
|
|
|
|
const char *Code = gettext(LanguageCode);
|
|
|
|
for (const char **lc = LanguageCodeList; *lc; lc++) {
|
|
|
|
if (ContainsCode(*lc, Code)) {
|
|
|
|
Code = *lc;
|
|
|
|
break;
|
|
|
|
}
|
2007-08-12 09:54:23 +02:00
|
|
|
}
|
2007-08-24 13:09:09 +02:00
|
|
|
LanguageCodes.Append(strdup(Code));
|
|
|
|
}
|
2007-08-11 14:27:02 +02:00
|
|
|
}
|
2007-08-19 14:34:44 +02:00
|
|
|
else {
|
|
|
|
esyslog("ERROR: too many locales - increase I18N_MAX_LANGUAGES!");
|
|
|
|
break;
|
|
|
|
}
|
2007-08-18 09:21:52 +02:00
|
|
|
}
|
2002-05-09 16:26:56 +02:00
|
|
|
}
|
2007-08-19 14:34:44 +02:00
|
|
|
SetEnvLanguage(LanguageLocales[CurrentLanguage]);
|
2007-08-11 12:39:06 +02:00
|
|
|
free(OldLocale);
|
2012-09-01 14:03:45 +02:00
|
|
|
dsyslog("found %d locales in %s", NumLocales - 1, *I18nLocaleDir);
|
2002-05-09 16:26:56 +02:00
|
|
|
}
|
2007-08-12 09:54:23 +02:00
|
|
|
// Prepare any known language codes for which there was no locale:
|
2020-06-15 15:57:32 +02:00
|
|
|
NumLanguages = NumLocales;
|
2007-08-12 09:54:23 +02:00
|
|
|
for (const char **lc = LanguageCodeList; *lc; lc++) {
|
|
|
|
bool Found = false;
|
|
|
|
for (int i = 0; i < LanguageCodes.Size(); i++) {
|
|
|
|
if (strcmp(*lc, LanguageCodes[i]) == 0) {
|
|
|
|
Found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!Found) {
|
|
|
|
dsyslog("no locale for language code '%s'", *lc);
|
2020-06-15 15:57:32 +02:00
|
|
|
NumLanguages++;
|
2007-08-12 09:54:23 +02:00
|
|
|
LanguageLocales.Append(strdup(I18N_DEFAULT_LOCALE));
|
|
|
|
LanguageNames.Append(strdup(*lc));
|
|
|
|
LanguageCodes.Append(strdup(*lc));
|
|
|
|
}
|
|
|
|
}
|
2020-06-15 15:57:32 +02:00
|
|
|
// Add special language codes and names:
|
|
|
|
for (const struct tSpecialLc *slc = SpecialLanguageCodeList; slc->Code; slc++) {
|
|
|
|
const char *TranslatedName = gettext(slc->Name);
|
|
|
|
LanguageNames.Append(strdup( TranslatedName != slc->Name ? TranslatedName : SkipContext(slc->Name)));
|
|
|
|
LanguageCodes.Append(strdup(slc->Code));
|
|
|
|
}
|
2002-05-09 16:26:56 +02:00
|
|
|
}
|
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
void I18nRegister(const char *Plugin)
|
|
|
|
{
|
2007-08-19 14:34:44 +02:00
|
|
|
cString Domain = cString::sprintf("vdr-%s", Plugin);
|
|
|
|
bindtextdomain(Domain, I18nLocaleDir);
|
2007-08-11 12:39:06 +02:00
|
|
|
}
|
2002-05-09 16:26:56 +02:00
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
void I18nSetLocale(const char *Locale)
|
|
|
|
{
|
2007-08-12 12:18:23 +02:00
|
|
|
if (Locale && *Locale) {
|
|
|
|
int i = LanguageLocales.Find(Locale);
|
|
|
|
if (i >= 0) {
|
|
|
|
CurrentLanguage = i;
|
2007-08-19 14:34:44 +02:00
|
|
|
SetEnvLanguage(Locale);
|
2020-06-15 15:57:32 +02:00
|
|
|
SetLanguageNames();
|
2007-08-12 12:18:23 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
dsyslog("unknown locale: '%s'", Locale);
|
2007-08-11 12:39:06 +02:00
|
|
|
}
|
|
|
|
}
|
2002-05-09 16:26:56 +02:00
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
int I18nCurrentLanguage(void)
|
|
|
|
{
|
|
|
|
return CurrentLanguage;
|
|
|
|
}
|
2007-06-10 13:02:43 +02:00
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
void I18nSetLanguage(int Language)
|
2007-06-10 13:02:43 +02:00
|
|
|
{
|
2020-06-15 15:57:32 +02:00
|
|
|
if (Language < NumLanguages) {
|
2007-08-11 12:39:06 +02:00
|
|
|
CurrentLanguage = Language;
|
|
|
|
I18nSetLocale(I18nLocale(CurrentLanguage));
|
2007-06-10 13:02:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2007-08-18 09:21:52 +02:00
|
|
|
int I18nNumLanguagesWithLocale(void)
|
|
|
|
{
|
|
|
|
return NumLocales;
|
|
|
|
}
|
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
const cStringList *I18nLanguages(void)
|
2002-05-09 16:26:56 +02:00
|
|
|
{
|
2007-08-11 12:39:06 +02:00
|
|
|
return &LanguageNames;
|
2002-05-09 16:26:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const char *I18nTranslate(const char *s, const char *Plugin)
|
2000-11-11 10:39:27 +01:00
|
|
|
{
|
2007-08-15 14:18:26 +02:00
|
|
|
if (!s)
|
|
|
|
return s;
|
2007-08-11 12:39:06 +02:00
|
|
|
if (CurrentLanguage) {
|
2007-08-24 14:03:47 +02:00
|
|
|
const char *t = Plugin ? dgettext(Plugin, s) : gettext(s);
|
2007-08-24 12:56:57 +02:00
|
|
|
if (t != s)
|
|
|
|
return t;
|
2000-11-11 10:39:27 +01:00
|
|
|
}
|
2007-08-24 12:56:57 +02:00
|
|
|
return SkipContext(s);
|
2000-11-11 10:39:27 +01:00
|
|
|
}
|
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
const char *I18nLocale(int Language)
|
2003-10-24 12:53:12 +02:00
|
|
|
{
|
2007-08-11 12:39:06 +02:00
|
|
|
return 0 <= Language && Language < LanguageLocales.Size() ? LanguageLocales[Language] : NULL;
|
2003-10-24 12:53:12 +02:00
|
|
|
}
|
2004-01-09 15:53:59 +01:00
|
|
|
|
2007-08-11 12:39:06 +02:00
|
|
|
const char *I18nLanguageCode(int Language)
|
2004-01-09 15:53:59 +01:00
|
|
|
{
|
2007-08-11 12:39:06 +02:00
|
|
|
return 0 <= Language && Language < LanguageCodes.Size() ? LanguageCodes[Language] : NULL;
|
2004-01-09 15:53:59 +01:00
|
|
|
}
|
|
|
|
|
2004-01-24 14:59:04 +01:00
|
|
|
int I18nLanguageIndex(const char *Code)
|
2004-01-09 15:53:59 +01:00
|
|
|
{
|
2007-08-11 12:39:06 +02:00
|
|
|
for (int i = 0; i < LanguageCodes.Size(); i++) {
|
2007-08-12 09:54:23 +02:00
|
|
|
if (ContainsCode(LanguageCodes[i], Code))
|
|
|
|
return i;
|
2004-01-09 15:53:59 +01:00
|
|
|
}
|
2004-01-24 14:59:04 +01:00
|
|
|
//dsyslog("unknown language code: '%s'", Code);
|
2004-01-09 15:53:59 +01:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2004-01-25 15:32:08 +01:00
|
|
|
const char *I18nNormalizeLanguageCode(const char *Code)
|
|
|
|
{
|
2006-10-08 11:28:15 +02:00
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
if (Code[i]) {
|
|
|
|
// ETSI EN 300 468 defines language codes as consisting of three letters
|
|
|
|
// according to ISO 639-2. This means that they are supposed to always consist
|
|
|
|
// of exactly three letters in the range a-z - no digits, UTF-8 or other
|
|
|
|
// funny characters. However, some broadcasters apparently don't have a
|
|
|
|
// copy of the DVB standard (or they do, but are perhaps unable to read it),
|
|
|
|
// so they put all sorts of non-standard stuff into the language codes,
|
|
|
|
// like nonsense as "2ch" or "A 1" (yes, they even go as far as using
|
|
|
|
// blanks!). Such things should go into the description of the EPG event's
|
|
|
|
// ComponentDescriptor.
|
|
|
|
// So, as a workaround for this broadcaster stupidity, let's ignore
|
|
|
|
// language codes with unprintable characters...
|
|
|
|
if (!isprint(Code[i])) {
|
|
|
|
//dsyslog("invalid language code: '%s'", Code);
|
|
|
|
return "???";
|
|
|
|
}
|
|
|
|
// ...and replace blanks with underlines (ok, this breaks the 'const'
|
|
|
|
// of the Code parameter - but hey, it's them who started this):
|
|
|
|
if (Code[i] == ' ')
|
|
|
|
*((char *)&Code[i]) = '_';
|
|
|
|
}
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
2004-01-25 15:32:08 +01:00
|
|
|
int n = I18nLanguageIndex(Code);
|
|
|
|
return n >= 0 ? I18nLanguageCode(n) : Code;
|
|
|
|
}
|
|
|
|
|
2005-09-04 14:48:39 +02:00
|
|
|
bool I18nIsPreferredLanguage(int *PreferredLanguages, const char *LanguageCode, int &OldPreference, int *Position)
|
2004-01-09 15:53:59 +01:00
|
|
|
{
|
2005-09-09 14:50:35 +02:00
|
|
|
int pos = 1;
|
2005-09-10 10:20:48 +02:00
|
|
|
bool found = false;
|
2005-09-09 14:50:35 +02:00
|
|
|
while (LanguageCode) {
|
|
|
|
int LanguageIndex = I18nLanguageIndex(LanguageCode);
|
2007-08-11 12:39:06 +02:00
|
|
|
for (int i = 0; i < LanguageCodes.Size(); i++) {
|
2005-09-09 14:50:35 +02:00
|
|
|
if (PreferredLanguages[i] < 0)
|
|
|
|
break; // the language is not a preferred one
|
|
|
|
if (PreferredLanguages[i] == LanguageIndex) {
|
|
|
|
if (OldPreference < 0 || i < OldPreference) {
|
|
|
|
OldPreference = i;
|
|
|
|
if (Position)
|
|
|
|
*Position = pos;
|
2005-09-10 10:20:48 +02:00
|
|
|
found = true;
|
2005-09-09 14:50:35 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2004-01-09 15:53:59 +01:00
|
|
|
}
|
2005-09-09 14:50:35 +02:00
|
|
|
if ((LanguageCode = strchr(LanguageCode, '+')) != NULL) {
|
|
|
|
LanguageCode++;
|
|
|
|
pos++;
|
|
|
|
}
|
|
|
|
else if (pos == 1 && Position)
|
|
|
|
*Position = 0;
|
|
|
|
}
|
2004-01-09 15:53:59 +01:00
|
|
|
if (OldPreference < 0) {
|
2007-08-11 12:39:06 +02:00
|
|
|
OldPreference = LanguageCodes.Size(); // higher than the maximum possible value
|
2004-01-09 15:53:59 +01:00
|
|
|
return true; // if we don't find a preferred one, we take the first one
|
|
|
|
}
|
2005-09-10 10:20:48 +02:00
|
|
|
return found;
|
2004-01-09 15:53:59 +01:00
|
|
|
}
|