[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-commits
Subject:    [kcm-grub2] /: *ADDED: Translation support for the GRUB menu.
From:       Konstantinos Smanis <konstantinos.smanis () gmail ! com>
Date:       2013-10-24 12:09:37
Message-ID: E1VZJjN-0005XQ-Rb () scm ! kde ! org
[Download RAW message or body]

Git commit bc64b83fb19171a2cf3d420304c612f192a29c5f by Konstantinos Smanis.
Committed on 24/10/2013 at 11:47.
Pushed by ksmanis into branch 'master'.

*ADDED: Translation support for the GRUB menu.

There are two translatable points in GRUB:
1. The GRUB menu which is shown when booting. To be more specific, this
   includes /only/ the instructions shown below the rectangular frame
   containing the menu. This translation is triggered with a
   'set lang=foobar' command in the GRUB menu file (/boot/grub/grub.cfg
   by default), where foobar is a translation catalog (*.mo) contained
   in the GRUB locale dir (/boot/grub/locale by default). The above
   command is usually generated by scripts in the /etc/grub.d/
   directory which process the 'LANG' [environment] variable.
2. The strings found in the GRUB menu file. To be more specific, this
   includes entries' titles, echo commands etc. These strings are
   generated by grub-mkconfig. This translation is triggered with the
   'LANGUAGE' [environment] variable.

The above variables can be either set in the environment running
grub-mkconfig or in GRUB's configuration file (/etc/default/grub by
default) for a more permanent effect.

Unfortunately, after extensive testing in various distributions (Debian,
Kubuntu, Fedora, openSUSE) there are some limitations concerning the
use of these variables:
1. LANGUAGE should be set exclusively from the list of translation
   catalogs in GRUB's locale dir.
2. LANG should be set exclusively from the list of locales as returned
   by 'locale -a'.

If LANG is not an available locale, LANGUAGE will be cancelled out. So,
in order for a full translation of the GRUB boot menu, we set LANGUAGE
to a proper locale, use the user's current locale as LANG (it will be
later manually overwritten so we don't mind; we just want a locale that
truely exists) and finally 'set lang=' with the same locale we used in
LANGUAGE.

This is not a very clean solution, but up to this point this is the best
I could make out from this situation that could work on as many distros
as possible. If anyone has more information on this, please let me know.

M  +4    -0    cmake/modules/GRUBPaths.cmake
M  +1    -0    config.h.cmake
M  +2    -1    src/common.h
M  +39   -0    src/helper/helper.cpp
M  +1    -0    src/helper/helper.h
M  +51   -2    src/kcm_grub2.cpp
M  +4    -0    src/kcm_grub2.h
M  +70   -56   ui/kcm_grub2.ui

http://commits.kde.org/kcm-grub2/bc64b83fb19171a2cf3d420304c612f192a29c5f

diff --git a/cmake/modules/GRUBPaths.cmake b/cmake/modules/GRUBPaths.cmake
index 7c30e1d..1ab4b6d 100644
--- a/cmake/modules/GRUBPaths.cmake
+++ b/cmake/modules/GRUBPaths.cmake
@@ -10,6 +10,7 @@ elseif(NOT (GRUB_INSTALL_EXE OR GRUB_MKCONFIG_EXE OR GRUB_PROBE_EXE \
                OR GRUB_SET_
         set(GRUB_CONFIG "/etc/default/grub" CACHE FILEPATH "GRUB configuration file \
                path.")
         set(GRUB_ENV "/boot/grub/grubenv" CACHE FILEPATH "GRUB environment file \
                path.")
         set(GRUB_MEMTEST "/etc/grub.d/20_memtest86+" CACHE FILEPATH "GRUB memtest \
file path.") +        set(GRUB_LOCALE "/boot/grub/locale/" CACHE PATH "GRUB locale \
                path.")
     else(GRUB_INSTALL_EXE AND GRUB_MKCONFIG_EXE AND GRUB_PROBE_EXE AND \
GRUB_SET_DEFAULT_EXE)  unset(GRUB_INSTALL_EXE CACHE)
         unset(GRUB_MKCONFIG_EXE CACHE)
@@ -24,6 +25,7 @@ elseif(NOT (GRUB_INSTALL_EXE OR GRUB_MKCONFIG_EXE OR GRUB_PROBE_EXE \
                OR GRUB_SET_
             set(GRUB_CONFIG "/etc/default/grub" CACHE FILEPATH "GRUB configuration \
                file path.")
             set(GRUB_ENV "/boot/grub2/grubenv" CACHE FILEPATH "GRUB environment file \
                path.")
             set(GRUB_MEMTEST "/etc/grub.d/20_memtest86+" CACHE FILEPATH "GRUB \
memtest file path.") +            set(GRUB_LOCALE "/boot/grub2/locale/" CACHE PATH \
                "GRUB locale path.")
         else(GRUB_INSTALL_EXE AND GRUB_MKCONFIG_EXE AND GRUB_PROBE_EXE AND \
GRUB_SET_DEFAULT_EXE)  unset(GRUB_INSTALL_EXE CACHE)
             unset(GRUB_MKCONFIG_EXE CACHE)
@@ -38,6 +40,7 @@ elseif(NOT (GRUB_INSTALL_EXE OR GRUB_MKCONFIG_EXE OR GRUB_PROBE_EXE \
                OR GRUB_SET_
                 set(GRUB_CONFIG "/etc/default/burg" CACHE FILEPATH "GRUB \
                configuration file path.")
                 set(GRUB_ENV "/boot/burg/burgenv" CACHE FILEPATH "GRUB environment \
                file path.")
                 set(GRUB_MEMTEST "/etc/burg.d/20_memtest86+" CACHE FILEPATH "GRUB \
memtest file path.") +                set(GRUB_LOCALE "/boot/burg/locale/" CACHE PATH \
                "GRUB locale path.")
             else(GRUB_INSTALL_EXE AND GRUB_MKCONFIG_EXE AND GRUB_PROBE_EXE AND \
                GRUB_SET_DEFAULT_EXE)
                 message(FATAL_ERROR "Could not automatically resolve GRUB paths. \
                Please specify all of them manually.")
             endif(GRUB_INSTALL_EXE AND GRUB_MKCONFIG_EXE AND GRUB_PROBE_EXE AND \
GRUB_SET_DEFAULT_EXE) @@ -56,4 +59,5 @@ message(STATUS "GRUB_MENU: ${GRUB_MENU}")
 message(STATUS "GRUB_CONFIG: ${GRUB_CONFIG}")
 message(STATUS "GRUB_ENV: ${GRUB_ENV}")
 message(STATUS "GRUB_MEMTEST: ${GRUB_MEMTEST}")
+message(STATUS "GRUB_LOCALE: ${GRUB_LOCALE}")
 message(STATUS "--------------------------------------------------------------------------")
                
diff --git a/config.h.cmake b/config.h.cmake
index 2270142..f5db630 100644
--- a/config.h.cmake
+++ b/config.h.cmake
@@ -17,5 +17,6 @@
 #define GRUB_CONFIG "@GRUB_CONFIG@"
 #define GRUB_ENV "@GRUB_ENV@"
 #define GRUB_MEMTEST "@GRUB_MEMTEST@"
+#define GRUB_LOCALE "@GRUB_LOCALE@"
 
 #endif
diff --git a/src/common.h b/src/common.h
index 153d876..92412da 100644
--- a/src/common.h
+++ b/src/common.h
@@ -27,7 +27,8 @@ enum LoadOperation {
     ConfigurationFile = 0x2,
     EnvironmentFile = 0x4,
     MemtestFile = 0x8,
-    Vbe = 0x10
+    Vbe = 0x10,
+    Locales = 0x20
 };
 Q_DECLARE_FLAGS(LoadOperations, LoadOperation)
 Q_DECLARE_OPERATORS_FOR_FLAGS(LoadOperations)
diff --git a/src/helper/helper.cpp b/src/helper/helper.cpp
index a40f879..34cb3a7 100644
--- a/src/helper/helper.cpp
+++ b/src/helper/helper.cpp
@@ -67,6 +67,33 @@ ActionReply Helper::executeCommand(const QStringList &command)
     reply.addData("output", process.readAll());
     return reply;
 }
+bool Helper::setLang(const QString &lang)
+{
+    QFile file(GRUB_MENU);
+    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+        kError() << "Failed to open file for reading:" << GRUB_MENU;
+        kError() << "Error code:" << file.error();
+        kError() << "Error description:" << file.errorString();
+        return false;
+    }
+    QString fileContents = QString::fromUtf8(file.readAll().constData());
+    file.close();
+    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+        kError() << "Failed to open file for writing:" << GRUB_MENU;
+        kError() << "Error code:" << file.error();
+        kError() << "Error description:" << file.errorString();
+        return false;
+    }
+    fileContents.replace(QRegExp(QLatin1String("(\\n\\s*set\\s+lang=)\\S*\\n")), \
QString(QLatin1String("\\1%1\n")).arg(lang)); +    if \
(file.write(fileContents.toUtf8()) == -1) { +        kError() << "Failed to write \
data to file:" << GRUB_MENU; +        kError() << "Error code:" << file.error();
+        kError() << "Error description:" << file.errorString();
+        return false;
+    }
+    file.close();
+    return true;
+}
 
 ActionReply Helper::defaults(QVariantMap args)
 {
@@ -182,6 +209,9 @@ ActionReply Helper::load(QVariantMap args)
         reply.addData("gfxmodes", gfxmodes);
     }
 #endif
+    if (operations.testFlag(Locales)) {
+        reply.addData(QLatin1String("locales"), \
QDir(GRUB_LOCALE).entryList(QStringList() << QLatin1String("*.mo"), \
QDir::Files).replaceInStrings(QRegExp(QLatin1String("\\.mo$")), QString())); +    }
     return reply;
 }
 ActionReply Helper::save(QVariantMap args)
@@ -213,10 +243,19 @@ ActionReply Helper::save(QVariantMap args)
         QFile::setPermissions(GRUB_MEMTEST, permissions);
     }
 
+    if (args.contains(QLatin1String("LANG"))) {
+        qputenv("LANG", args.value(QLatin1String("LANG")).toByteArray());
+    }
     ActionReply grub_mkconfigReply = executeCommand(QStringList() << \
GRUB_MKCONFIG_EXE << "-o" << GRUB_MENU);  if (grub_mkconfigReply.failed()) {
         return grub_mkconfigReply;
     }
+    if (args.contains(QLatin1String("LANGUAGE"))) {
+        if (!setLang(args.value(QLatin1String("LANGUAGE")).toString())) {
+            kError() << "An error occured while setting the language for the GRUB \
menu."; +            kError() << "The GRUB menu will not be properly translated!";
+        }
+    }
 
     ActionReply grub_set_defaultReply = executeCommand(QStringList() << \
GRUB_SET_DEFAULT_EXE << rawDefaultEntry);  if (grub_set_defaultReply.failed()) {
diff --git a/src/helper/helper.h b/src/helper/helper.h
index 11e8935..e63b41f 100644
--- a/src/helper/helper.h
+++ b/src/helper/helper.h
@@ -29,6 +29,7 @@ public:
     Helper();
 private:
     ActionReply executeCommand(const QStringList &command);
+    bool setLang(const QString &lang);
 public Q_SLOTS:
     ActionReply defaults(QVariantMap args);
     ActionReply install(QVariantMap args);
diff --git a/src/kcm_grub2.cpp b/src/kcm_grub2.cpp
index 68a0ef9..87c7cc5 100644
--- a/src/kcm_grub2.cpp
+++ b/src/kcm_grub2.cpp
@@ -185,6 +185,13 @@ void KCMGRUB2::load()
         kWarning() << "Invalid GRUB_TIMEOUT value";
     }
 
+    showLocales();
+    int languageIndex = \
ui->kcombobox_language->findData(unquoteWord(m_settings.value(QLatin1String("LANGUAGE"))));
 +    if (languageIndex != -1) {
+        ui->kcombobox_language->setCurrentIndex(languageIndex);
+    } else {
+        kWarning() << "Invalid LANGUAGE value";
+    }
     ui->checkBox_recovery->setChecked(unquoteWord(m_settings.value("GRUB_DISABLE_RECOVERY")).compare("true") \
!= 0);  ui->checkBox_memtest->setVisible(m_memtest);
     ui->checkBox_memtest->setChecked(m_memtestOn);
@@ -305,6 +312,14 @@ void KCMGRUB2::save()
             m_settings["GRUB_TIMEOUT"] = "-1";
         }
     }
+    if (m_dirtyBits.testBit(grubLocaleDirty)) {
+        int langIndex = ui->kcombobox_language->currentIndex();
+        if (langIndex > 0) {
+            m_settings[QLatin1String("LANGUAGE")] = \
ui->kcombobox_language->itemData(langIndex).toString(); +        } else {
+            m_settings.remove(QLatin1String("LANGUAGE"));
+        }
+    }
     if (m_dirtyBits.testBit(grubDisableRecoveryDirty)) {
         if (ui->checkBox_recovery->isChecked()) {
             m_settings.remove("GRUB_DISABLE_RECOVERY");
@@ -453,6 +468,10 @@ void KCMGRUB2::save()
     saveAction.setHelperID("org.kde.kcontrol.kcmgrub2");
     saveAction.addArgument("rawConfigFileContents", \
                configFileContents.toLocal8Bit());
     saveAction.addArgument("rawDefaultEntry", !m_entries.isEmpty() ? grubDefault : \
m_settings.value("GRUB_DEFAULT").toLocal8Bit()); +    if \
(ui->kcombobox_language->currentIndex() > 0) { +        \
saveAction.addArgument(QLatin1String("LANG"), qgetenv("LANG")); +        \
saveAction.addArgument(QLatin1String("LANGUAGE"), \
m_settings.value(QLatin1String("LANGUAGE"))); +    }
     if (m_dirtyBits.testBit(memtestDirty)) {
         saveAction.addArgument("memtest", ui->checkBox_memtest->isChecked());
     }
@@ -529,6 +548,11 @@ void KCMGRUB2::slotGrubTimeoutChanged()
     m_dirtyBits.setBit(grubTimeoutDirty);
     emit changed(true);
 }
+void KCMGRUB2::slotGrubLanguageChanged()
+{
+    m_dirtyBits.setBit(grubLocaleDirty);
+    emit changed(true);
+}
 void KCMGRUB2::slotGrubDisableRecoveryChanged()
 {
     m_dirtyBits.setBit(grubDisableRecoveryDirty);
@@ -880,6 +904,7 @@ void KCMGRUB2::setupConnections()
     connect(ui->radioButton_timeout, SIGNAL(clicked(bool)), this, \
                SLOT(slotGrubTimeoutChanged()));
     connect(ui->spinBox_timeout, SIGNAL(valueChanged(int)), this, \
SLOT(slotGrubTimeoutChanged()));  
+    connect(ui->kcombobox_language, SIGNAL(activated(int)), this, \
                SLOT(slotGrubLanguageChanged()));
     connect(ui->checkBox_recovery, SIGNAL(clicked(bool)), this, \
                SLOT(slotGrubDisableRecoveryChanged()));
     connect(ui->checkBox_memtest, SIGNAL(clicked(bool)), this, \
                SLOT(slotMemtestChanged()));
     connect(ui->checkBox_osProber, SIGNAL(clicked(bool)), this, \
SLOT(slotGrubDisableOsProberChanged())); @@ -926,7 +951,7 @@ bool \
KCMGRUB2::readFile(const QString &fileName, QByteArray &fileContents)  {
     QFile file(fileName);
     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
-        kDebug() << "Failed to read file:" << fileName;
+        kDebug() << "Failed to open file for reading:" << fileName;
         kDebug() << "Error code:" << file.error();
         kDebug() << "Error description:" << file.errorString();
         kDebug() << "The helper will now attempt to read this file.";
@@ -964,6 +989,11 @@ void KCMGRUB2::readAll()
 #if HAVE_HD
     operations |= Vbe;
 #endif
+    if (QFileInfo(GRUB_LOCALE).isReadable()) {
+        m_locales = QDir(GRUB_LOCALE).entryList(QStringList() << \
QLatin1String("*.mo"), \
QDir::Files).replaceInStrings(QRegExp(QLatin1String("\\.mo$")), QString()); +    } \
else { +        operations |= Locales;
+    }
 
     if (operations) {
         Action loadAction("org.kde.kcontrol.kcmgrub2.load");
@@ -1018,9 +1048,28 @@ void KCMGRUB2::readAll()
         if (operations.testFlag(Vbe)) {
             m_resolutions = \
reply.data().value(QLatin1String("gfxmodes")).toStringList();  }
+        if (operations.testFlag(Locales)) {
+            m_locales = reply.data().value(QLatin1String("locales")).toStringList();
+        }
     }
 }
 
+void KCMGRUB2::showLocales()
+{
+    ui->kcombobox_language->clear();
+    ui->kcombobox_language->addItem(i18nc("@item:inlistbox", "No translation"), \
QString()); +
+    Q_FOREACH(const QString &locale, m_locales) {
+        QString language = KGlobal::locale()->languageCodeToName(locale);
+        if (language.isEmpty()) {
+            language = \
KGlobal::locale()->languageCodeToName(locale.split('@').first()); +            if \
(language.isEmpty()) { +                language = \
KGlobal::locale()->languageCodeToName(locale.split('@').first().split('_').first()); \
+            } +        }
+        ui->kcombobox_language->addItem(QString("%1 (%2)").arg(language, locale), \
locale); +    }
+}
 void KCMGRUB2::sortResolutions()
 {
     for (int i = 0; i < m_resolutions.size(); i++) {
@@ -1212,7 +1261,7 @@ void KCMGRUB2::parseSettings(const QString &config)
     m_settings.clear();
     while (!stream.atEnd()) {
         line = stream.readLine().trimmed();
-        if (line.startsWith(QLatin1String("GRUB_"))) {
+        if (line.contains(QRegExp(QLatin1String("^(GRUB_|LANGUAGE=)")))) {
             m_settings[line.section('=', 0, 0)] = line.section('=', 1);
         }
     }
diff --git a/src/kcm_grub2.h b/src/kcm_grub2.h
index 014847f..92b39ab 100644
--- a/src/kcm_grub2.h
+++ b/src/kcm_grub2.h
@@ -56,6 +56,7 @@ private Q_SLOTS:
     void slotGrubHiddenTimeoutQuietChanged();
     void slotGrubTimeoutToggled(bool checked);
     void slotGrubTimeoutChanged();
+    void slotGrubLanguageChanged();
     void slotGrubDisableRecoveryChanged();
     void slotMemtestChanged();
     void slotGrubDisableOsProberChanged();
@@ -87,6 +88,7 @@ private:
     bool readFile(const QString &fileName, QByteArray &fileContents);
     void readAll();
 
+    void showLocales();
     void sortResolutions();
     void showResolutions();
 
@@ -103,6 +105,7 @@ private:
         grubHiddenTimeoutDirty,
         grubHiddenTimeoutQuietDirty,
         grubTimeoutDirty,
+        grubLocaleDirty,
         grubDisableRecoveryDirty,
         memtestDirty,
         grubDisableOsProberDirty,
@@ -132,6 +135,7 @@ private:
     bool m_memtestOn;
     QHash<QString, QString> m_devices;
     QStringList m_resolutions;
+    QStringList m_locales;
 };
 
 #endif
diff --git a/ui/kcm_grub2.ui b/ui/kcm_grub2.ui
index 8a4bc41..1d84e8c 100644
--- a/ui/kcm_grub2.ui
+++ b/ui/kcm_grub2.ui
@@ -20,7 +20,48 @@
       <attribute name="title">
        <string comment="@title:tab Refers to settings.">General</string>
       </attribute>
-      <layout class="QGridLayout" name="gridLayout_5">
+      <layout class="QGridLayout" name="gridLayout_4">
+       <item row="0" column="0">
+        <widget class="QGroupBox" name="groupBox_default">
+         <property name="title">
+          <string comment="@title:group">Default Entry</string>
+         </property>
+         <layout class="QFormLayout" name="formLayout">
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_default">
+            <property name="text">
+             <string comment="@label:listbox">Default Entry:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="1">
+           <layout class="QHBoxLayout" name="horizontalLayout">
+            <item>
+             <widget class="KComboBox" name="kcombobox_default">
+              <property name="sizeAdjustPolicy">
+               <enum>QComboBox::AdjustToContents</enum>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KPushButton" name="kpushbutton_remove">
+              <property name="text">
+               <string comment="@action:button">Remove Old Entries</string>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </item>
+          <item row="1" column="0" colspan="2">
+           <widget class="QCheckBox" name="checkBox_savedefault">
+            <property name="text">
+             <string comment="@option:check">The next booted entry will become \
default</string> +            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
        <item row="1" column="0">
         <widget class="QGroupBox" name="groupBox_timeout">
          <property name="title">
@@ -153,40 +194,41 @@
          </layout>
         </widget>
        </item>
-       <item row="3" column="0">
-        <spacer name="verticalSpacer">
-         <property name="orientation">
-          <enum>Qt::Vertical</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>20</width>
-           <height>43</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
        <item row="2" column="0">
         <widget class="QGroupBox" name="groupBox_entries">
          <property name="title">
           <string comment="@title:group">Generated Entries</string>
          </property>
-         <layout class="QGridLayout" name="gridLayout_4">
+         <layout class="QFormLayout" name="formLayout_6">
           <item row="0" column="0">
+           <widget class="QLabel" name="label_language">
+            <property name="text">
+             <string>Language:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="1">
+           <widget class="KComboBox" name="kcombobox_language">
+            <property name="sizeAdjustPolicy">
+             <enum>QComboBox::AdjustToContents</enum>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0" colspan="2">
            <widget class="QCheckBox" name="checkBox_recovery">
             <property name="text">
              <string comment="@option:check">Generate recovery entries</string>
             </property>
            </widget>
           </item>
-          <item row="1" column="0">
+          <item row="2" column="0" colspan="2">
            <widget class="QCheckBox" name="checkBox_memtest">
             <property name="text">
              <string comment="@option:check">Generate memtest entries</string>
             </property>
            </widget>
           </item>
-          <item row="2" column="0">
+          <item row="3" column="0" colspan="2">
            <widget class="QCheckBox" name="checkBox_osProber">
             <property name="text">
              <string comment="@option:check">Probe for operating systems</string>
@@ -196,46 +238,18 @@
          </layout>
         </widget>
        </item>
-       <item row="0" column="0">
-        <widget class="QGroupBox" name="groupBox_default">
-         <property name="title">
-          <string comment="@title:group">Default Entry</string>
+       <item row="3" column="0">
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
          </property>
-         <layout class="QFormLayout" name="formLayout">
-          <item row="0" column="0">
-           <widget class="QLabel" name="label_default">
-            <property name="text">
-             <string comment="@label:listbox">Default Entry:</string>
-            </property>
-           </widget>
-          </item>
-          <item row="0" column="1">
-           <layout class="QHBoxLayout" name="horizontalLayout">
-            <item>
-             <widget class="KComboBox" name="kcombobox_default">
-              <property name="sizeAdjustPolicy">
-               <enum>QComboBox::AdjustToContents</enum>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="KPushButton" name="kpushbutton_remove">
-              <property name="text">
-               <string comment="@action:button">Remove Old Entries</string>
-              </property>
-             </widget>
-            </item>
-           </layout>
-          </item>
-          <item row="1" column="0" colspan="2">
-           <widget class="QCheckBox" name="checkBox_savedefault">
-            <property name="text">
-             <string comment="@option:check">The next booted entry will become \
                default</string>
-            </property>
-           </widget>
-          </item>
-         </layout>
-        </widget>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>43</height>
+          </size>
+         </property>
+        </spacer>
        </item>
       </layout>
      </widget>


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic