diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54486423f..4b3070379 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,7 +13,6 @@ before_script: test: script: - cd app - - ./tools/langs-list-check.py - ./tools/check-string-format.py - cd .. - ./gradlew assemble -PdisablePreDex diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md new file mode 100644 index 000000000..a0a7ae3dd --- /dev/null +++ b/RELEASE_CHECKLIST.md @@ -0,0 +1,23 @@ + +# Release Checklist + +This is the things that need to happen for all releases, alpha or stable: + +* pull translations from Weblate: ./tools/pull-trans.sh + +* rebase Weblate in its web interface, since we squash commits + +* update `versionCode` in _app/build.gradle_ + +* make signed tag with version name + +* update _metadata/org.fdroid.fdroid.txt_ in _fdroiddata_ + +## Stable releases + +For stable releases, there are a couple more steps to do __before__ +making the release tag: + +* update CHANGELOG.md + +* run `./tools/trim-incomplete-translations-for-release.py` diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 4d78f0af9..ed95dedef 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -27,7 +27,6 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -36,16 +35,15 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.StrictMode; -import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import android.widget.Toast; - import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiskCache; import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; - +import info.guardianproject.netcipher.NetCipher; +import info.guardianproject.netcipher.proxy.OrbotHelper; import org.acra.ACRA; import org.acra.ReportingInteractionMode; import org.acra.annotation.ReportsCrashes; @@ -59,17 +57,13 @@ import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.net.ImageLoaderForUIL; import org.fdroid.fdroid.net.WifiStateChangeService; +import sun.net.www.protocol.bluetooth.Handler; import java.net.URL; import java.net.URLStreamHandler; import java.net.URLStreamHandlerFactory; import java.security.Security; import java.util.List; -import java.util.Locale; - -import info.guardianproject.netcipher.NetCipher; -import info.guardianproject.netcipher.proxy.OrbotHelper; -import sun.net.www.protocol.bluetooth.Handler; @ReportsCrashes(mailTo = "reports@f-droid.org", mode = ReportingInteractionMode.DIALOG, @@ -82,8 +76,6 @@ public class FDroidApp extends Application { public static final String SYSTEM_DIR_NAME = Environment.getRootDirectory().getAbsolutePath(); - private static Locale locale; - // for the local repo on this device, all static since there is only one public static volatile int port; public static volatile String ipAddressString; @@ -181,25 +173,10 @@ public class FDroidApp extends Application { repo = new Repo(); } - public void updateLanguage() { - Context ctx = getBaseContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); - String lang = prefs.getString(Preferences.PREF_LANGUAGE, ""); - locale = Utils.getLocaleFromAndroidLangTag(lang); - applyLanguage(); - } - - private void applyLanguage() { - Context ctx = getBaseContext(); - Configuration cfg = new Configuration(); - cfg.locale = locale == null ? Locale.getDefault() : locale; - ctx.getResources().updateConfiguration(cfg, null); - } - @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - applyLanguage(); + Languages.setLanguage(this, Preferences.get().getLangauge(), false); } @Override @@ -215,7 +192,9 @@ public class FDroidApp extends Application { .penaltyLog() .build()); } - updateLanguage(); + Preferences.setup(this); + Languages.setup(getClass(), R.string.pref_language_default); + Languages.setLanguage(this, Preferences.get().getLangauge(), false); ACRA.init(this); if (isAcraProcess()) { @@ -224,7 +203,6 @@ public class FDroidApp extends Application { PRNGFixes.apply(); - Preferences.setup(this); curTheme = Preferences.get().getTheme(); Preferences.get().configureProxy(); @@ -325,13 +303,13 @@ public class FDroidApp extends Application { /** * Asks if the current process is "org.fdroid.fdroid:acra". - * + *

* This is helpful for bailing out of the {@link FDroidApp#onCreate} method early, preventing * problems that arise from executing the code twice. This happens due to the `android:process` * statement in AndroidManifest.xml causes another process to be created to run * {@link org.fdroid.fdroid.acra.CrashReportActivity}. This was causing lots of things to be * started/run twice including {@link CleanCacheService} and {@link WifiStateChangeService}. - * + *

* Note that it is not perfect, because some devices seem to not provide a list of running app * processes when asked. In such situations, F-Droid may regress to the behaviour where some * services may run twice and thus cause weirdness or slowness. However that is probably better diff --git a/app/src/main/java/org/fdroid/fdroid/Languages.java b/app/src/main/java/org/fdroid/fdroid/Languages.java new file mode 100644 index 000000000..5891d9123 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/Languages.java @@ -0,0 +1,285 @@ +package org.fdroid.fdroid; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.text.TextUtils; +import android.util.DisplayMetrics; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public final class Languages { + public static final String TAG = "Languages"; + + public static final String USE_SYSTEM_DEFAULT = ""; + + private static final Locale DEFAULT_LOCALE; + private static final Locale TIBETAN = new Locale("bo"); + private static final Locale CHINESE_HONG_KONG = new Locale("zh", "HK"); + private static final String DEFAULT_STRING = "System Default"; + + private static Locale locale; + private static Languages singleton; + private static Class clazz; + private static int resId; + private static Map tmpMap = new TreeMap<>(); + private static Map nameMap; + + static { + DEFAULT_LOCALE = Locale.getDefault(); + } + + private Languages(Activity activity) { + AssetManager assets = activity.getAssets(); + Configuration config = activity.getResources().getConfiguration(); + // Resources() requires DisplayMetrics, but they are only needed for drawables + DisplayMetrics ignored = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(ignored); + Resources resources; + Set localeSet = new LinkedHashSet<>(); + for (Locale locale : LOCALES_TO_TEST) { + config.locale = locale; + resources = new Resources(assets, ignored, config); + if (!TextUtils.equals(DEFAULT_STRING, resources.getString(resId)) + || locale.equals(Locale.ENGLISH)) { + localeSet.add(locale); + } + } + for (Locale locale : localeSet) { + if (locale.equals(TIBETAN)) { + // include English name for devices without Tibetan font support + tmpMap.put(TIBETAN.getLanguage(), "Tibetan བོད་སྐད།"); // Tibetan + } else if (locale.equals(Locale.SIMPLIFIED_CHINESE)) { + tmpMap.put(Locale.SIMPLIFIED_CHINESE.toString(), "中文 (中国)"); // Chinese (China) + } else if (locale.equals(Locale.TRADITIONAL_CHINESE)) { + tmpMap.put(Locale.TRADITIONAL_CHINESE.toString(), "中文 (台灣)"); // Chinese (Taiwan) + } else if (locale.equals(CHINESE_HONG_KONG)) { + tmpMap.put(CHINESE_HONG_KONG.toString(), "中文 (香港)"); // Chinese (Hong Kong) + } else { + tmpMap.put(locale.getLanguage(), capitalize(locale.getDisplayLanguage(locale))); + } + } + + /* SYSTEM_DEFAULT is a fake one for displaying in a chooser menu. */ + localeSet.add(null); + tmpMap.put(USE_SYSTEM_DEFAULT, activity.getString(resId)); + nameMap = Collections.unmodifiableMap(tmpMap); + } + + /** + * Get the instance of {@link Languages} to work with, providing the + * {@link Activity} that is will be working as part of, as well as the + * {@code resId} that has the exact string "Use System Default", + * i.e. {@code R.string.use_system_default}. + *

+ * That string resource {@code resId} is also used to find the supported + * translations: if an included translation has a translated string that + * matches that {@code resId}, then that language will be included as a + * supported language. + * + * @param clazz the {@link Class} of the default {@code Activity}, + * usually the main {@code Activity} from where the + * Settings is launched from. + * @param resId the string resource ID to for the string "System Default", + * e.g. {@code R.string.pref_language_default} + */ + public static void setup(Class clazz, int resId) { + if (Languages.clazz == null) { + Languages.clazz = clazz; + Languages.resId = resId; + } else { + throw new RuntimeException("Languages singleton was already initialized, duplicate call to Languages.setup()!"); + } + } + + /** + * @param activity the {@link Activity} this is working as part of + * @return the singleton to work with + */ + public static Languages get(Activity activity) { + if (singleton == null) { + singleton = new Languages(activity); + } + return singleton; + } + + @TargetApi(17) + public static void setLanguage(final ContextWrapper contextWrapper, String language, boolean refresh) { + if (locale != null && TextUtils.equals(locale.getLanguage(), language) && (!refresh)) { + return; // already configured + } else if (language == null || language.equals(USE_SYSTEM_DEFAULT)) { + locale = DEFAULT_LOCALE; + } else { + /* handle locales with the country in it, i.e. zh_CN, zh_TW, etc */ + String[] localeSplit = language.split("_"); + if (localeSplit.length > 1) { + locale = new Locale(localeSplit[0], localeSplit[1]); + } else { + locale = new Locale(language); + } + } + + final Resources resources = contextWrapper.getBaseContext().getResources(); + Configuration config = resources.getConfiguration(); + if (Build.VERSION.SDK_INT >= 17) { + config.setLocale(locale); + } else { + config.locale = locale; + } + resources.updateConfiguration(config, resources.getDisplayMetrics()); + Locale.setDefault(locale); + + } + + /** + * Force reload the {@link Activity to make language changes take effect.} + * + * @param activity the {@code Activity} to force reload + */ + public static void forceChangeLanguage(Activity activity) { + Intent intent = activity.getIntent(); + if (intent == null) { // when launched as LAUNCHER + return; + } + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + activity.finish(); + activity.overridePendingTransition(0, 0); + activity.startActivity(intent); + activity.overridePendingTransition(0, 0); + } + + /** + * @return the name of the language based on the locale. + */ + public String getName(String locale) { + String ret = nameMap.get(locale); + // if no match, try to return a more general name (i.e. English for en_IN) + if (ret == null && locale.contains("_")) { + ret = nameMap.get(locale.split("_")[0]); + } + return ret; + } + + /** + * @return an array of the names of all the supported languages, sorted to + * match what is returned by {@link Languages#getSupportedLocales()}. + */ + public String[] getAllNames() { + return nameMap.values().toArray(new String[nameMap.size()]); + } + + public int getPosition(Locale locale) { + String localeName = locale.getLanguage(); + int i = 0; + for (String key : nameMap.keySet()) { + if (TextUtils.equals(key, localeName)) { + return i; + } else { + i++; + } + } + return -1; + } + + /** + * @return sorted list of supported locales. + */ + public String[] getSupportedLocales() { + Set keys = nameMap.keySet(); + return keys.toArray(new String[keys.size()]); + } + + private String capitalize(final String line) { + return Character.toUpperCase(line.charAt(0)) + line.substring(1); + } + + private static final Locale[] LOCALES_TO_TEST = { + Locale.ENGLISH, + Locale.FRENCH, + Locale.GERMAN, + Locale.ITALIAN, + Locale.JAPANESE, + Locale.KOREAN, + Locale.SIMPLIFIED_CHINESE, + Locale.TRADITIONAL_CHINESE, + CHINESE_HONG_KONG, + TIBETAN, + new Locale("af"), + new Locale("am"), + new Locale("ar"), + new Locale("az"), + new Locale("be"), + new Locale("bg"), + new Locale("bn"), + new Locale("ca"), + new Locale("cs"), + new Locale("da"), + new Locale("el"), + new Locale("es"), + new Locale("et"), + new Locale("eu"), + new Locale("fa"), + new Locale("fi"), + new Locale("gl"), + new Locale("hi"), + new Locale("hr"), + new Locale("hu"), + new Locale("hy"), + new Locale("in"), + new Locale("hy"), + new Locale("in"), + new Locale("is"), + new Locale("it"), + new Locale("iw"), + new Locale("ka"), + new Locale("kk"), + new Locale("km"), + new Locale("kn"), + new Locale("ky"), + new Locale("lo"), + new Locale("lt"), + new Locale("lv"), + new Locale("mk"), + new Locale("ml"), + new Locale("mn"), + new Locale("mr"), + new Locale("ms"), + new Locale("my"), + new Locale("nb"), + new Locale("ne"), + new Locale("nl"), + new Locale("pl"), + new Locale("pt"), + new Locale("rm"), + new Locale("ro"), + new Locale("ru"), + new Locale("si"), + new Locale("sk"), + new Locale("sl"), + new Locale("sn"), + new Locale("sr"), + new Locale("sv"), + new Locale("sw"), + new Locale("ta"), + new Locale("te"), + new Locale("th"), + new Locale("tl"), + new Locale("tr"), + new Locale("uk"), + new Locale("ur"), + new Locale("uz"), + new Locale("vi"), + new Locale("zu"), + }; + +} diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index a3b8d7936..92977927e 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -229,6 +229,10 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh .replaceAll(" ", "-"); } + public String getLangauge() { + return preferences.getString(Preferences.PREF_LANGUAGE, ""); + } + public String getLocalRepoName() { return preferences.getString(PREF_LOCAL_REPO_NAME, getDefaultLocalRepoName()); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java index 4574cb177..ed50137d5 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java +++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/PreferencesFragment.java @@ -12,19 +12,18 @@ import android.preference.Preference; import android.preference.PreferenceCategory; import android.support.v4.preference.PreferenceFragment; import android.text.TextUtils; - +import com.geecko.QuickLyric.view.AppCompatListPreference; +import info.guardianproject.netcipher.NetCipher; +import info.guardianproject.netcipher.proxy.OrbotHelper; import org.fdroid.fdroid.AppDetails2; import org.fdroid.fdroid.CleanCacheService; -import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Languages; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.installer.InstallHistoryService; import org.fdroid.fdroid.installer.PrivilegedInstaller; -import info.guardianproject.netcipher.NetCipher; -import info.guardianproject.netcipher.proxy.OrbotHelper; - public class PreferencesFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -62,6 +61,12 @@ public class PreferencesFragment extends PreferenceFragment enableProxyCheckPref = (CheckBoxPreference) findPreference(Preferences.PREF_ENABLE_PROXY); updateAutoDownloadPref = findPreference(Preferences.PREF_AUTO_DOWNLOAD_INSTALL_UPDATES); updatePrivilegedExtensionPref = findPreference(Preferences.PREF_UNINSTALL_PRIVILEGED_APP); + + AppCompatListPreference languagePref = (AppCompatListPreference) findPreference(Preferences.PREF_LANGUAGE); + Languages languages = Languages.get(getActivity()); + languagePref.setDefaultValue(Languages.USE_SYSTEM_DEFAULT); + languagePref.setEntries(languages.getAllNames()); + languagePref.setEntryValues(languages.getSupportedLocales()); } private void checkSummary(String key, int resId) { @@ -136,8 +141,9 @@ public class PreferencesFragment extends PreferenceFragment case Preferences.PREF_LANGUAGE: entrySummary(key); if (changing) { - // TODO: Ask MainActivity to restart itself. - ((FDroidApp) getActivity().getApplication()).updateLanguage(); + Activity activity = getActivity(); + Languages.setLanguage(activity, Preferences.get().getLangauge(), false); + Languages.forceChangeLanguage(activity); } break; diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 505cefb68..1de33474f 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -44,126 +44,4 @@ night - - - en - af - ar - ast - be - bg - ca - cs - da - de - el - eo - es - et - eu - fa - fi - fr - gl - he - hi - hr - hu - hy - id - is - it - ja - ko - lt - lv - mk - my - nb - nl - pl - pt-rBR - pt-rPT - ro - ru - sc - sk - sl - sn - sq - sr - sv - ta - th - tr - ug - uk - ur - vi - zh-rCN - zh-rHK - zh-rTW - - - - @string/pref_language_default - English - Afrikaans - ﺎﻠﻋﺮﺒﻳﺓ - Asturian - белорусский - Български - Català - Čeština - Dansk - Deutsch - Ελληνικά - Esperanto - Español - Eesti - Euskara - ﻑﺍﺮﺳی - Suomi - Français - Galego - עברית - हिन्दी - Hrvatski - Magyar - հայերեն - Bahasa Indonesia - Íslenska - Italiano - 日本語 - 한국어 - Lietuvių - Latviešu - македонски - မြန်မာစာ - Norsk bokmål - Nederlands - Polski - Português (Brasil) - Português (Portugal) - Română - Русский - Sardinian - Slovenčina - Slovenščina - ChiSona - Shqip - Српски - Svenska - தமிழ் - ไทย - Türkçe - ﺉۇﻲﻏۇﺭچە - Українська - اردو - Tiếng Việt - 中文 (中国) - 中文 (香港) - 中文 (台湾) - - diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 6850f2e76..8f560d234 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -43,10 +43,7 @@ + android:key="language"/> 4: + if float(row[4]) > 75.0: + continue + locale = row[1] + if '_' in locale: + codes = locale.split('_') + if codes[1] == 'Hans': + codes[1] = 'CN' + elif codes[1] == 'Hant': + codes[1] = 'TW' + locale = codes[0] + '-r' + codes[1] + translation_file = 'app/src/main/res/values-' + locale + '/strings.xml' + percent = str(int(float(row[4]))) + '%' + print('Removing incomplete file: (' + percent + ')\t', + translation_file) + os.remove(os.path.join(projectbasedir, translation_file)) + repo.index.remove([translation_file, ]) + if len(percent) == 2: + msg += ' ' + msg += percent + ' ' + row[1] + ' ' + row[0] + '\n' + +repo.index.commit(msg)