From 81b3c721bc3c6fbdcf424adfea7731010357feed Mon Sep 17 00:00:00 2001 From: Hugo Melder Date: Wed, 7 Aug 2024 16:26:16 +0200 Subject: [PATCH] NSUserDefaults: Retrieve native UI languages from Windows and Android System APIs (#426) * Create NSString Win32Additions Category * NSUserDefaults: Fetch Windows UI language information * NSUserDefaults: Add winnls include * Android Native UI Language * NSUserDefaults: Replace incorrect separator on Windows * NSProcessInfo: BCP-47 Note * GSConfig: Bump MinGW WINVER to Vista * NSString+Win32Additions: Do not add array to arp twice * NSUserDefaults: Increase default length --- Headers/GNUstepBase/GSConfig.h.in | 2 +- Source/NSProcessInfo.m | 50 +++++++++++++- Source/NSUserDefaults.m | 95 +++++++++++++++++++++++++- Source/win32/GNUmakefile | 1 + Source/win32/NSString+Win32Additions.h | 40 +++++++++++ Source/win32/NSString+Win32Additions.m | 45 ++++++++++++ 6 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 Source/win32/NSString+Win32Additions.h create mode 100644 Source/win32/NSString+Win32Additions.m diff --git a/Headers/GNUstepBase/GSConfig.h.in b/Headers/GNUstepBase/GSConfig.h.in index 39b7a712e4..9c709f0c2d 100644 --- a/Headers/GNUstepBase/GSConfig.h.in +++ b/Headers/GNUstepBase/GSConfig.h.in @@ -304,7 +304,7 @@ typedef struct { # if defined(__MINGW__) # include -# define GS_WINVER Windows2000 +# define GS_WINVER WindowsVista # elif defined(_MSC_VER) # include # define GS_WINVER _WIN32_WINNT_WIN10 diff --git a/Source/NSProcessInfo.m b/Source/NSProcessInfo.m index d7a0449065..9ca552e205 100644 --- a/Source/NSProcessInfo.m +++ b/Source/NSProcessInfo.m @@ -1605,7 +1605,45 @@ - (void) performExpiringActivityWithReason: (NSString *)reason } } - // get current time zone + char *localeList = NULL; + #if __ANDROID_API__ >= 24 + // get locales ordered by user preference + jclass localeListCls = (*env)->FindClass(env, "android/os/LocaleList"); + jmethodID localeListGetDefaultMethod = (*env)->GetStaticMethodID(env, localeListCls, "getDefault", "()Landroid/os/LocaleList;"); + jobject localeListObj = (*env)->CallStaticObjectMethod(env, localeListCls, localeListGetDefaultMethod); + + if (localeCls) { + // Retrieve string representation of the locale list + jmethodID localeListToLanguageTagsMethod = (*env)->GetMethodID(env, localeListCls, "toLanguageTags", "()Ljava/lang/String;"); + jstring localeListJava = (*env)->CallObjectMethod(env, localeListObj, localeListToLanguageTagsMethod); + + if (localeListJava) { + const char *localeListOrig = (*env)->GetStringUTFChars(env, localeIdJava, NULL); + + // Some devices return with it enclosed in []'s so check if both exists before + // removing to ensure it is formatted correctly + if (localeListOrig[0] == '[' && localeListOrig[strlen(localeListOrig) - 1] == ']') { + localeList = strdup(localeListOrig + 1); + localeList[strlen(localeList) - 1] = '\0'; + } else { + localeList = strdup(localeListOrig); + } + + // NOTE: This is an IETF BCP 47 language tag and may not correspond exactly tocorrespond ll-CC format + // e.g. gsw-u-sd-chzh is a valid BCP 47 language tag, but uses an ISO 639-3 subtag to classify the language. + // There is no easy fix to this, as we use ISO 639-2 subtags internally. + for (int i = 0; localeList[i]; i++) { + if (localeList[i] == '-') { + localeList[i] = '_'; + } + } + + (*env)->ReleaseStringUTFChars(env, localeListJava, localeListOrig); + } + } + #endif + + jclass timezoneCls = (*env)->FindClass(env, "java/util/TimeZone"); jmethodID timezoneDefaultMethod = (*env)->GetStaticMethodID(env, timezoneCls, "getDefault", "()Ljava/util/TimeZone;"); jmethodID timezoneIdMethod = (*env)->GetMethodID(env, timezoneCls, "getID", "()Ljava/lang/String;"); @@ -1613,20 +1651,28 @@ - (void) performExpiringActivityWithReason: (NSString *)reason jstring timezoneIdJava = (*env)->CallObjectMethod(env, timezoneObj, timezoneIdMethod); const char *timezoneId = (*env)->GetStringUTFChars(env, timezoneIdJava, NULL); + char *localeListValue = ""; + if (localeList) { + localeListValue = localeList; + } + // initialize process with these options char *argv[] = { arg0, "-Locale", localeId, "-Local Time Zone", (char *)timezoneId, + "-GSAndroidLocaleList", localeListValue, "-GSLogSyslog", "YES" // use syslog (available via logcat) instead of stdout/stderr (not available on Android) }; GSInitializeProcessAndroidWithArgs(env, context, sizeof(argv)/sizeof(char *), argv, NULL); free(arg0); + free(localeId); + free(localeList); (*env)->ReleaseStringUTFChars(env, packageCodePathJava, packageCodePath); (*env)->ReleaseStringUTFChars(env, packageNameJava, packageName); - (*env)->ReleaseStringUTFChars(env, localeIdJava, localeId); + (*env)->ReleaseStringUTFChars(env, localeIdJava, localeIdOrig); (*env)->ReleaseStringUTFChars(env, timezoneIdJava, timezoneId); } diff --git a/Source/NSUserDefaults.m b/Source/NSUserDefaults.m index ebbb6087ec..c0464ba81d 100644 --- a/Source/NSUserDefaults.m +++ b/Source/NSUserDefaults.m @@ -56,6 +56,10 @@ #import "GNUstepBase/NSString+GNUstepBase.h" #if defined(_WIN32) +#import "win32/NSString+Win32Additions.h" + +#include + /* Fake interface to avoid compiler warnings */ @interface NSUserDefaultsWin32 : NSUserDefaults @@ -355,6 +359,94 @@ - (BOOL) synchronize; { NSMutableArray *names = [NSMutableArray arrayWithCapacity: 10]; + #ifdef WIN32 + NSEnumerator *enumerator; + NSArray *languages; + NSString *locale; + BOOL ret; + + unsigned long numberOfLanguages = 0; + unsigned long length = 7; + unsigned long factor = sizeof(wchar_t); + wchar_t *buffer = malloc(length * factor); + if (!buffer) + { + return names; + } + + /* Returns a wchar_t list of languages in the form ll-CC, where ll is the + * two-letter language code, and CC is the two-letter country code. + */ + ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, + buffer, &length); + if (!ret) + { + length = 0; + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER && + GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, + NULL, &length)) { + wchar_t *oldBuffer = buffer; + buffer = realloc(buffer, length * factor); + if (!buffer) + { + free(oldBuffer); + return names; + } + + ret = GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, buffer, &length); + if (!ret) + { + free(buffer); + return names; + } + } + } + + languages = [NSString arrayFromWCharList:buffer length:length]; + enumerator = [languages objectEnumerator]; + free(buffer); + + while (nil != (locale = [enumerator nextObject])) + { + /* Replace "-" Separator with "_" */ + locale = [locale stringByReplacingOccurrencesOfString:@"-" withString:@"_"]; + [names addObjectsFromArray: GSLanguagesFromLocale(locale)]; + } + #elif defined(__ANDROID__) + // When running on Android, the process must be correctly initialized + // with GSInitializeProcessAndroid (See NSProcessInfo). + // + // If the minimum API level is 24 or higher, the user-prefered locales + // are retrieved from the Android system and passed as GSAndroidLocaleList + // process argument + NSArray *args = [[NSProcessInfo processInfo] arguments]; + NSEnumerator *enumerator = [args objectEnumerator]; + NSString *key = nil; + NSString *localeList = nil; + + [enumerator nextObject]; // Skip process name. + while (nil != (key = [enumerator nextObject])) + { + if ([key isEqualToString:@"-GSAndroidLocaleList"]) + { + localeList = [enumerator nextObject]; + break; + } + } + + // The locale list is a comma-separated list of locales of form ll-CC + if (localeList != nil) + { + NSString *locale; + NSArray *locales = [localeList componentsSeparatedByString: @","]; + + enumerator = [locales objectEnumerator]; + while (nil != (locale = [enumerator nextObject])) + { + [names addObjectsFromArray: GSLanguagesFromLocale(locale)]; + } + } + #else // Add the languages listed in the LANGUAGE environment variable // (a non-POSIX GNU extension) { @@ -371,7 +463,7 @@ - (BOOL) synchronize; } } } - + // If LANGUAGES did not yield any languages, try LC_MESSAGES if ([names count] == 0) @@ -383,6 +475,7 @@ - (BOOL) synchronize; [names addObjectsFromArray: GSLanguagesFromLocale(locale)]; } } + #endif return names; } diff --git a/Source/win32/GNUmakefile b/Source/win32/GNUmakefile index da7fad1fd9..ae1923b9a8 100644 --- a/Source/win32/GNUmakefile +++ b/Source/win32/GNUmakefile @@ -37,6 +37,7 @@ win32_OBJC_FILES =\ NSMessagePortNameServer.m \ NSStream.m \ NSUserDefaults.m \ + NSString+Win32Additions.m\ -include Makefile.preamble diff --git a/Source/win32/NSString+Win32Additions.h b/Source/win32/NSString+Win32Additions.h new file mode 100644 index 0000000000..3c46fef1a4 --- /dev/null +++ b/Source/win32/NSString+Win32Additions.h @@ -0,0 +1,40 @@ +/** Category for converting Windows Strings + Copyright (C) 1998 Free Software Foundation, Inc. + + Written by: Hugo Melder + Created: July 2024 + + This file is part of the GNUstep Base Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ + +#import + +#include + +/** + * Converts a wchar_t list to an array of strings. + * The list is NULL-delimited and terminated by two NULL (wchar_t) characters. + * + * The encoding is Unicode (UTF-16LE). + */ +@interface NSString (Win32Additions) + ++ (GS_GENERIC_CLASS(NSArray, NSString *) *) arrayFromWCharList: (wchar_t *)list + length: (unsigned long)length; + +@end diff --git a/Source/win32/NSString+Win32Additions.m b/Source/win32/NSString+Win32Additions.m new file mode 100644 index 0000000000..d52f1e8688 --- /dev/null +++ b/Source/win32/NSString+Win32Additions.m @@ -0,0 +1,45 @@ +/** Category for converting Windows Strings + Copyright (C) 1998 Free Software Foundation, Inc. + + Written by: Hugo Melder + Created: July 2024 + + This file is part of the GNUstep Base Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ + +#import "NSString+Win32Additions.h" + +@implementation NSString (Win32Additions) + ++ (GS_GENERIC_CLASS(NSArray, NSString *) *) arrayFromWCharList: (wchar_t *)list + length: (unsigned long)length +{ + NSString *string; + GS_GENERIC_CLASS(NSArray, NSString *) * array; + + string = [[NSString alloc] initWithBytes: list + length: (length - 2) * sizeof(wchar_t) + encoding: NSUTF16LittleEndianStringEncoding]; + + array = [string componentsSeparatedByString: @"\0"]; + RELEASE(string); + + return array; +} + +@end