Skip to content

Commit

Permalink
Fix some i18n (localization) issues with Web Extensions.
Browse files Browse the repository at this point in the history
https://webkit.org/b/264211
rdar://problem/117948039

Reviewed by Brian Weinstein.

* Remove a FIXME for <https://webkit.org/b/261047> Handle multiple unique identifiers for localization.
  This wasn't needed after all since the @@extension_id message can't be used in the manifest.
* Supply the @@extension_id key with the extension's base URL host in the WebProcess.
* Suppress errors for file not found when attempting to discover localization message files.
* Don't attempt to read the same file twice if the default_locale matches the language or regional name.
* Don't support locale predefined messages when there is no default_locale.
* Bail early in more places if there are no localized strings.
* Removed some unused NSCoding key strings.
* Added more tests to cover these changes.

* Source/WebKit/Shared/Extensions/_WKWebExtensionLocalization.h:
* Source/WebKit/Shared/Extensions/_WKWebExtensionLocalization.mm:
(-[_WKWebExtensionLocalization initWithWebExtension:]):
(-[_WKWebExtensionLocalization initWithLocalizedDictionary:uniqueIdentifier:]): Added.
(-[_WKWebExtensionLocalization initWithRegionalLocalization:languageLocalization:defaultLocalization:withBestLocale:uniqueIdentifier:]):
(-[_WKWebExtensionLocalization localizedDictionaryForDictionary:]):
(-[_WKWebExtensionLocalization localizedStringForKey:withPlaceholders:]):
(-[_WKWebExtensionLocalization _localizedArrayForArray:]):
(-[_WKWebExtensionLocalization _localizationDictionaryForWebExtension:withLocale:]):
(-[_WKWebExtensionLocalization _predefinedMessages]):
(-[_WKWebExtensionLocalization _predefinedMessagesForLocale:]): Deleted.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm:
(WebKit::WebExtension::resourceStringForPath):
(WebKit::WebExtension::resourceDataForPath):
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionURLSchemeHandlerCocoa.mm:
(WebKit::WebExtensionURLSchemeHandler::platformStartTask):
* Source/WebKit/UIProcess/Extensions/WebExtension.h:
* Source/WebKit/WebProcess/Extensions/Cocoa/WebExtensionContextProxyCocoa.mm:
(WebKit::WebExtensionContextProxy::getOrCreate):
(WebKit::WebExtensionContextProxy::parseLocalization):
* Source/WebKit/WebProcess/Extensions/WebExtensionContextProxy.h:
* Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPILocalization.mm:
(TestWebKitAPI::TEST):

Canonical link: https://commits.webkit.org/270279@main
  • Loading branch information
xeenon committed Nov 6, 2023
1 parent 7524670 commit ad7f913
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable, copy) NSDictionary *localizationDictionary;

- (instancetype)initWithWebExtension:(WebKit::WebExtension&)webExtension;
- (instancetype)initWithLocalizedDictionary:(nullable NSDictionary<NSString *, NSDictionary *> *)localizedDictionary uniqueIdentifier:(NSString *)uniqueIdentifier;
- (instancetype)initWithRegionalLocalization:(nullable NSDictionary<NSString *, NSDictionary *> *)regionalLocalization languageLocalization:(nullable NSDictionary<NSString *, NSDictionary *> *)languageLocalization defaultLocalization:(nullable NSDictionary<NSString *, NSDictionary *> *)defaultLocalization withBestLocale:(nullable NSString *)localeString uniqueIdentifier:(nullable NSString *)uniqueIdentifier NS_DESIGNATED_INITIALIZER;

- (NSDictionary<NSString *, id> *)localizedDictionaryForDictionary:(NSDictionary<NSString *, id> *)dictionary;
Expand Down
97 changes: 63 additions & 34 deletions Source/WebKit/Shared/Extensions/_WKWebExtensionLocalization.mm
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,6 @@
static NSString * const predefinedMessageValueTextEdgeLeft = @"left";
static NSString * const predefinedMessageValueTextEdgeRight = @"right";


// NSCoding keys
static NSString * const localizationDictionaryCodingKey = @"localizationDictionary";
static NSString * const localeStringDictionaryCodingKey = @"localeString";
static NSString * const localeCodingKey = @"locale";
static NSString * const uniqueIdentifierCodingKey = @"uniqueIdentifier";

using LocalizationDictionary = NSDictionary<NSString *, NSDictionary *>;
using PlaceholderDictionary = NSDictionary<NSString *, NSDictionary<NSString *, NSString *> *>;

Expand Down Expand Up @@ -94,7 +87,7 @@ - (instancetype)initWithWebExtension:(WebExtension&)webExtension
NSString *defaultLocaleString = webExtension.defaultLocale().localeIdentifier;

if (!defaultLocaleString.length) {
RELEASE_LOG_INFO(Extensions, "Not loading localization for extension %{private}@. No default locale provided.", webExtension.displayName());
RELEASE_LOG_DEBUG(Extensions, "No default locale provided");
return [self initWithRegionalLocalization:nil languageLocalization:nil defaultLocalization:nil withBestLocale:nil uniqueIdentifier:nil];
}

Expand All @@ -106,20 +99,40 @@ - (instancetype)initWithWebExtension:(WebExtension&)webExtension
NSString *bestLocaleString;

LocalizationDictionary *defaultLocaleDictionary = [self _localizationDictionaryForWebExtension:webExtension withLocale:defaultLocaleString];
if (defaultLocaleDictionary)
if (defaultLocaleDictionary) {
RELEASE_LOG_DEBUG(Extensions, "Default locale available for %{public}@", defaultLocaleString);
bestLocaleString = defaultLocaleString;
}

LocalizationDictionary *languageDictionary = [self _localizationDictionaryForWebExtension:webExtension withLocale:languageCode];
if (languageDictionary)
bestLocaleString = languageCode;
LocalizationDictionary *languageDictionary;
if (![languageCode isEqualToString:defaultLocaleString]) {
if ((languageDictionary = [self _localizationDictionaryForWebExtension:webExtension withLocale:languageCode])) {
RELEASE_LOG_DEBUG(Extensions, "Language locale available for %{public}@", languageCode);
bestLocaleString = languageCode;
}
}

LocalizationDictionary *regionalDictionary = [self _localizationDictionaryForWebExtension:webExtension withLocale:regionalLocaleString];
if (regionalDictionary)
bestLocaleString = defaultLocaleString;
LocalizationDictionary *regionalDictionary;
if (![regionalLocaleString isEqualToString:defaultLocaleString]) {
if ((regionalDictionary = [self _localizationDictionaryForWebExtension:webExtension withLocale:regionalLocaleString])) {
RELEASE_LOG_DEBUG(Extensions, "Regional locale available for %{public}@", regionalLocaleString);
bestLocaleString = regionalLocaleString;
}
}

RELEASE_LOG_DEBUG(Extensions, "Best locale is %{public}@", bestLocaleString ?: @"undefined");

return [self initWithRegionalLocalization:regionalDictionary languageLocalization:languageDictionary defaultLocalization:defaultLocaleDictionary withBestLocale:bestLocaleString uniqueIdentifier:nil];
}

- (instancetype)initWithLocalizedDictionary:(NSDictionary<NSString *, NSDictionary *> *)localizedDictionary uniqueIdentifier:(NSString *)uniqueIdentifier
{
ASSERT(uniqueIdentifier);

NSString *localeString = localizedDictionary[predefinedMessageUILocale][messageKey];
return [self initWithRegionalLocalization:localizedDictionary languageLocalization:nil defaultLocalization:nil withBestLocale:localeString uniqueIdentifier:uniqueIdentifier];
}

- (instancetype)initWithRegionalLocalization:(LocalizationDictionary *)regionalLocalization languageLocalization:(LocalizationDictionary *)languageLocalization defaultLocalization:(LocalizationDictionary *)defaultLocalization withBestLocale:(NSString *)localeString uniqueIdentifier:(NSString *)uniqueIdentifier
{
if (!(self = [super init]))
Expand All @@ -129,20 +142,22 @@ - (instancetype)initWithRegionalLocalization:(LocalizationDictionary *)regionalL
_localeString = localeString;
_uniqueIdentifier = uniqueIdentifier;

LocalizationDictionary *localizationDictionary = [self _predefinedMessagesForLocale:_locale];
LocalizationDictionary *localizationDictionary = self._predefinedMessages;
localizationDictionary = dictionaryWithLowercaseKeys(localizationDictionary);
localizationDictionary = mergeDictionaries(localizationDictionary, dictionaryWithLowercaseKeys(regionalLocalization));
localizationDictionary = mergeDictionaries(localizationDictionary, dictionaryWithLowercaseKeys(languageLocalization));
localizationDictionary = mergeDictionaries(localizationDictionary, dictionaryWithLowercaseKeys(defaultLocalization));

ASSERT(localizationDictionary);

_localizationDictionary = localizationDictionary;

return self;
}

- (NSDictionary<NSString *, id> *)localizedDictionaryForDictionary:(NSDictionary<NSString *, id> *)dictionary
{
if (!_localizationDictionary)
if (!_localizationDictionary.count)
return dictionary;

NSMutableDictionary<NSString *, id> *localizedDictionary = [dictionary mutableCopy];
Expand All @@ -166,6 +181,9 @@ - (NSString *)localizedStringForKey:(NSString *)key withPlaceholders:(NSArray<NS
if (placeholders.count > 9)
return nil;

if (!_localizationDictionary.count)
return @"";

LocalizationDictionary *stringDictionary = objectForKey<NSDictionary>(_localizationDictionary, key.lowercaseString);

NSString *localizedString = objectForKey<NSString>(stringDictionary, messageKey);
Expand All @@ -185,14 +203,16 @@ - (NSString *)localizedStringForKey:(NSString *)key withPlaceholders:(NSArray<NS

- (NSArray *)_localizedArrayForArray:(NSArray *)array
{
if (!_localizationDictionary.count)
return array;

return mapObjects(array, ^id(id key, id value) {
if ([value isKindOfClass:NSString.class])
return [self localizedStringForString:value];
if ([value isKindOfClass:NSArray.class])
return [self _localizedArrayForArray:value];
if ([value isKindOfClass:NSDictionary.class])
return [self localizedDictionaryForDictionary:value];

return value;
});
}
Expand All @@ -217,37 +237,41 @@ - (NSString *)localizedStringForString:(NSString *)sourceString
return localizedString;
}

- (LocalizationDictionary *)_localizationDictionaryForWebExtension:(WebExtension&)webExtension withLocale:(NSString *)locale
- (LocalizationDictionary *)_localizationDictionaryForWebExtension:(WebExtension&)webExtension withLocale:(NSString *)localeString
{
NSString *path = [NSString stringWithFormat:pathToJSONFile, locale];
NSData *data = [NSData dataWithData:webExtension.resourceDataForPath(path)];
auto *path = [NSString stringWithFormat:pathToJSONFile, localeString];
auto *data = [NSData dataWithData:webExtension.resourceDataForPath(path, WebExtension::CacheResult::No, WebExtension::SuppressNotFoundErrors::Yes)];
return parseJSON(data);
}

- (LocalizationDictionary *)_predefinedMessagesForLocale:(NSLocale *)locale
- (LocalizationDictionary *)_predefinedMessages
{
NSDictionary *predefindedMessage;
if ([NSParagraphStyle defaultWritingDirectionForLanguage:_locale.languageCode] == NSWritingDirectionLeftToRight) {
predefindedMessage = @{
predefinedMessageUILocale: @{ messageKey: _localeString ?: @"" },
NSMutableDictionary *predefinedMessages;

if (_locale && [NSParagraphStyle defaultWritingDirectionForLanguage:_locale.languageCode] == NSWritingDirectionLeftToRight) {
predefinedMessages = [@{
predefinedMessageLanguageDirection: @{ messageKey: predefinedMessageValueLeftToRight },
predefinedMessageLanguageDirectionReversed: @{ messageKey: predefinedMessageValueRightToLeft },
predefinedMessageTextLeadingEdge: @{ messageKey: predefinedMessageValueTextEdgeLeft },
predefinedMessageTextTrailingEdge: @{ messageKey: predefinedMessageValueTextEdgeRight },
};
} else {
predefindedMessage = @{
predefinedMessageUILocale: @{ messageKey: _localeString ?: @"" },
} mutableCopy];
} else if (_locale) {
predefinedMessages = [@{
predefinedMessageLanguageDirection: @{ messageKey: predefinedMessageValueRightToLeft },
predefinedMessageLanguageDirectionReversed: @{ messageKey: predefinedMessageValueLeftToRight },
predefinedMessageTextLeadingEdge: @{ messageKey: predefinedMessageValueTextEdgeRight },
predefinedMessageTextTrailingEdge: @{ messageKey: predefinedMessageValueTextEdgeLeft },
};
}
} mutableCopy];
} else
predefinedMessages = [NSMutableDictionary dictionary];

// FIXME: <https://webkit.org/b/261047> Handle multiple unique identifiers for localization.
if (_localeString)
predefinedMessages[predefinedMessageUILocale] = @{ messageKey: _localeString };

return predefindedMessage;
if (_uniqueIdentifier)
predefinedMessages[predefinedMessageExtensionID] = @{ messageKey: _uniqueIdentifier };

return [predefinedMessages copy];
}

- (NSString *)_stringByReplacingNamedPlaceholdersInString:(NSString *)string withNamedPlaceholders:(PlaceholderDictionary *)namedPlaceholders
Expand Down Expand Up @@ -300,6 +324,11 @@ - (instancetype)initWithWebExtension:(WebExtension&)extension
return [self initWithRegionalLocalization:nil languageLocalization:nil defaultLocalization:nil withBestLocale:nil uniqueIdentifier:nil];
}

- (instancetype)initWithLocalizedDictionary:(NSDictionary<NSString *, NSDictionary *> *)localizedDictionary uniqueIdentifier:(NSString *)uniqueIdentifier
{
return [self initWithRegionalLocalization:nil languageLocalization:nil defaultLocalization:nil withBestLocale:nil uniqueIdentifier:nil];
}

- (instancetype)initWithRegionalLocalization:(LocalizationDictionary *)regionalLocalization languageLocalization:(LocalizationDictionary *)languageLocalization defaultLocalization:(LocalizationDictionary *)defaultLocalization withBestLocale:(NSString *)localeString uniqueIdentifier:(NSString *)uniqueIdentifier
{
return nil;
Expand Down
12 changes: 7 additions & 5 deletions Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@
return result;
}

NSString *WebExtension::resourceStringForPath(NSString *path, CacheResult cacheResult)
NSString *WebExtension::resourceStringForPath(NSString *path, CacheResult cacheResult, SuppressNotFoundErrors suppressErrors)
{
ASSERT(path);

Expand All @@ -349,7 +349,7 @@
if ([path isEqualToString:generatedBackgroundPageFilename])
return generatedBackgroundContent();

NSData *data = resourceDataForPath(path, CacheResult::No);
NSData *data = resourceDataForPath(path, CacheResult::No, suppressErrors);

NSString *string;
[NSString stringEncodingForData:data encodingOptions:nil convertedString:&string usedLossyConversion:nil];
Expand All @@ -365,7 +365,7 @@
return string;
}

NSData *WebExtension::resourceDataForPath(NSString *path, CacheResult cacheResult)
NSData *WebExtension::resourceDataForPath(NSString *path, CacheResult cacheResult, SuppressNotFoundErrors suppressErrors)
{
ASSERT(path);

Expand Down Expand Up @@ -403,14 +403,16 @@

NSURL *resourceURL = resourceFileURLForPath(path);
if (!resourceURL) {
recordError(createError(Error::ResourceNotFound, WEB_UI_FORMAT_CFSTRING("Unable to find \"%@\" in the extension’s resources. It is an invalid path.", "WKWebExtensionErrorResourceNotFound description with invalid file path", (__bridge CFStringRef)path)));
if (suppressErrors == SuppressNotFoundErrors::No)
recordError(createError(Error::ResourceNotFound, WEB_UI_FORMAT_CFSTRING("Unable to find \"%@\" in the extension’s resources. It is an invalid path.", "WKWebExtensionErrorResourceNotFound description with invalid file path", (__bridge CFStringRef)path)));
return nil;
}

NSError *fileReadError;
NSData *resultData = [NSData dataWithContentsOfURL:resourceURL options:NSDataReadingMappedIfSafe error:&fileReadError];
if (!resultData) {
recordError(createError(Error::ResourceNotFound, WEB_UI_FORMAT_CFSTRING("Unable to find \"%@\" in the extension’s resources.", "WKWebExtensionErrorResourceNotFound description with file name", (__bridge CFStringRef)path), fileReadError));
if (suppressErrors == SuppressNotFoundErrors::No)
recordError(createError(Error::ResourceNotFound, WEB_UI_FORMAT_CFSTRING("Unable to find \"%@\" in the extension’s resources.", "WKWebExtensionErrorResourceNotFound description with file name", (__bridge CFStringRef)path), fileReadError));
return nil;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
}
}

NSData *fileData = extensionContext->extension().resourceDataForPath(requestURL.path().toString());
auto *fileData = extensionContext->extension().resourceDataForPath(requestURL.path().toString());
if (!fileData) {
task.didComplete([NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]);
return;
Expand All @@ -114,22 +114,19 @@
}
}

NSString *mimeType = [UTType typeWithFilenameExtension:((NSURL *)requestURL).pathExtension].preferredMIMEType;
auto *mimeType = [UTType typeWithFilenameExtension:((NSURL *)requestURL).pathExtension].preferredMIMEType;
if (!mimeType)
mimeType = @"application/octet-stream";

if ([mimeType isEqualToString:@"text/css"]) {
_WKWebExtensionLocalization *localization = extensionContext->extension().localization();
if (!localization.uniqueIdentifier)
localization.uniqueIdentifier = extensionContext->uniqueIdentifier();

// FIXME: <https://webkit.org/b/252628> Only attempt to localize CSS files if we notice a localization wildcard in the file's NSData.
NSString *stylesheetContents = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];
auto *localization = extensionContext->extension().localization();
auto *stylesheetContents = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];
stylesheetContents = [localization localizedStringForString:stylesheetContents];
fileData = [stylesheetContents dataUsingEncoding:NSUTF8StringEncoding];
}

NSHTTPURLResponse *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:requestURL statusCode:200 HTTPVersion:nil headerFields:@{
auto *urlResponse = [[NSHTTPURLResponse alloc] initWithURL:requestURL statusCode:200 HTTPVersion:nil headerFields:@{
@"Access-Control-Allow-Origin": @"*",
@"Content-Security-Policy": extensionContext->extension().contentSecurityPolicy(),
@"Content-Length": [NSString stringWithFormat:@"%zu", (size_t)fileData.length],
Expand Down
5 changes: 3 additions & 2 deletions Source/WebKit/UIProcess/Extensions/WebExtension.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class WebExtension : public API::ObjectImpl<API::Object::Type::WebExtension>, pu

enum class CacheResult : bool { No, Yes };
enum class SuppressNotification : bool { No, Yes };
enum class SuppressNotFoundErrors : bool { No, Yes };

enum class Error : uint8_t {
Unknown = 1,
Expand Down Expand Up @@ -164,8 +165,8 @@ class WebExtension : public API::ObjectImpl<API::Object::Type::WebExtension>, pu

UTType *resourceTypeForPath(NSString *);

NSString *resourceStringForPath(NSString *, CacheResult = CacheResult::No);
NSData *resourceDataForPath(NSString *, CacheResult = CacheResult::No);
NSString *resourceStringForPath(NSString *, CacheResult = CacheResult::No, SuppressNotFoundErrors = SuppressNotFoundErrors::No);
NSData *resourceDataForPath(NSString *, CacheResult = CacheResult::No, SuppressNotFoundErrors = SuppressNotFoundErrors::No);

_WKWebExtensionLocalization *localization();
NSLocale *defaultLocale();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
auto updateProperties = [&](WebExtensionContextProxy& context) {
context.m_baseURL = parameters.baseURL;
context.m_uniqueIdentifier = parameters.uniqueIdentifier;
context.m_localization = parseLocalization(parameters.localizationJSON.get());
context.m_localization = parseLocalization(parameters.localizationJSON.get(), parameters.baseURL);
context.m_manifest = parseJSON(parameters.manifestJSON.get());
context.m_manifestVersion = parameters.manifestVersion;
context.m_testingMode = parameters.testingMode;
Expand Down Expand Up @@ -120,13 +120,9 @@
return result.releaseNonNull();
}

_WKWebExtensionLocalization *WebExtensionContextProxy::parseLocalization(API::Data& json)
_WKWebExtensionLocalization *WebExtensionContextProxy::parseLocalization(API::Data& json, const URL& baseURL)
{
NSDictionary *localizedDictionary = parseJSON(json);
if (!localizedDictionary)
return nil;

return [[_WKWebExtensionLocalization alloc] initWithRegionalLocalization:localizedDictionary languageLocalization:nil defaultLocalization:nil withBestLocale:localizedDictionary[@"@@ui_locale"][@"message"] uniqueIdentifier:nil];
return [[_WKWebExtensionLocalization alloc] initWithLocalizedDictionary:parseJSON(json) uniqueIdentifier:baseURL.host().toString()];
}

} // namespace WebKit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ class WebExtensionContextProxy final : public RefCounted<WebExtensionContextProx

_WKWebExtensionLocalization *localization() { return m_localization.get(); }

static _WKWebExtensionLocalization *parseLocalization(API::Data&);

bool inTestingMode() { return m_testingMode; }

static WebCore::DOMWrapperWorld& mainWorld() { return WebCore::mainThreadNormalWorld(); }
Expand Down Expand Up @@ -117,6 +115,8 @@ class WebExtensionContextProxy final : public RefCounted<WebExtensionContextProx
private:
explicit WebExtensionContextProxy(const WebExtensionContextParameters&);

static _WKWebExtensionLocalization *parseLocalization(API::Data&, const URL& baseURL);

// Action
void dispatchActionClickedEvent(const std::optional<WebExtensionTabParameters>&);

Expand Down
Loading

0 comments on commit ad7f913

Please sign in to comment.