From a27aaf8bd0b55a22bcc6727e71f1fbd3c18ab5a9 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 17:11:29 +0200 Subject: [PATCH 1/8] Fixed: CPRuleEditor word-by-word translation issue (#1834) --- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 170 +++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index 12bbbe514..a0fc8d264 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -94,4 +94,174 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) return aString; } +#pragma mark - Whole Sentence Formatting & Reordering Helpers + +// Constructs the English-matching format representation of a single view +- (CPString)_englishRepresentationForView:(id)aView +{ + if ([aView isKindOfClass:[CPPopUpButton class]]) + { + var selectedItem = [aView selectedItem]; + if (selectedItem) + { + var originalTitle = [selectedItem representedObject] || selectedItem._originalTitle || [selectedItem title]; + return "%[" + originalTitle + "]@"; + } + return "%[]@"; + } + else if ([aView isKindOfClass:[CPTextField class]] && ![aView isEditable]) + { + return [aView stringValue]; + } + else + { + return "%@"; + } +} + +// Builds the current formatting lookup key (e.g. "%[property]@ %[is]@ %@") +- (CPString)formattingKeyForViews:(NSArray *)views +{ + var keyParts = []; + var count = [views count]; + for (var i = 0; i < count; i++) + { + var view = [views objectAtIndex:i]; + [keyParts addObject:[self _englishRepresentationForView:view]]; + } + return [keyParts componentsJoinedByString:@" "]; +} + +// Localizes all popup menu options within the row under their proper context +- (void)localizeMenuItemsForViews:(NSArray *)views +{ + var count = [views count]; + for (var i = 0; i < count; i++) + { + var view = [views objectAtIndex:i]; + if ([view isKindOfClass:[CPPopUpButton class]]) + { + var menuItems = [view itemArray]; + var menuItemsCount = [menuItems count]; + var selectedItem = [view selectedItem]; + + for (var j = 0; j < menuItemsCount; j++) + { + var item = [menuItems objectAtIndex:j]; + + if (!item._originalTitle) + { + item._originalTitle = [item title]; + } + + // Temporarily select item to formulate the unique localization key + [view selectItem:item]; + + var tempKey = [self formattingKeyForViews:views]; + var tempPattern = [self localizedStringForString:tempKey]; + + if (tempPattern !== tempKey) + { + // Search for this view position's translation inside the pattern (e.g. %2$[son iguales]@) + var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; + var match; + while ((match = regex.exec(tempPattern)) !== null) + { + var position = parseInt(match[1], 10) - 1; + var translatedValue = match[2]; + + if (position === i && translatedValue) + { + [item setTitle:translatedValue]; + break; + } + } + } + } + + // Restore initial selection + if (selectedItem) + { + [view selectItem:selectedItem]; + } + } + } +} + +// Translates labels and reorders the active subviews based on positional formatting string +- (NSArray *)localizeAndReorderViews:(NSArray *)views +{ + var key = [self formattingKeyForViews:views]; + var localizedPattern = [self localizedStringForString:key]; + + if (localizedPattern === key) + { + return views; + } + + var newViews = [CPMutableArray array]; + + // Pattern to extract index and translation: %1$[translation]@ or %1$@ + var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; + var lastIndex = 0; + var match; + + while ((match = regex.exec(localizedPattern)) !== null) + { + // 1. Insert any leading static text (e.g. " y ") + var literalText = localizedPattern.substring(lastIndex, match.index); + if (literalText.length > 0) + { + var label = [CPTextField labelWithString:literalText]; + [newViews addObject:label]; + } + + // 2. Identify the original view corresponding to the positional index + var position = parseInt(match[1], 10) - 1; + var translatedValue = match[2]; + + if (position >= 0 && position < [views count]) + { + var originalView = [views objectAtIndex:position]; + + if (translatedValue !== undefined && translatedValue !== null) + { + if ([originalView isKindOfClass:[CPPopUpButton class]]) + { + var selectedItem = [originalView selectedItem]; + if (selectedItem) + { + if (!selectedItem._originalTitle) + { + selectedItem._originalTitle = [selectedItem title]; + } + [selectedItem setTitle:translatedValue]; + } + } + else if (typeof originalView.setStringValue === "function") + { + [originalView setStringValue:translatedValue]; + } + } + + [newViews addObject:originalView]; + } + + lastIndex = regex.lastIndex; + } + + // 3. Append trailing static text + if (lastIndex < localizedPattern.length) + { + var literalText = localizedPattern.substring(lastIndex); + if (literalText.length > 0) + { + var label = [CPTextField labelWithString:literalText]; + [newViews addObject:label]; + } + } + + return newViews; +} + @end From d7315773e1c1c0472325301df8f17cc1bdd02272 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 17:15:41 +0200 Subject: [PATCH 2/8] fixed: syntax issues --- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index a0fc8d264..db08bd739 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -120,7 +120,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } // Builds the current formatting lookup key (e.g. "%[property]@ %[is]@ %@") -- (CPString)formattingKeyForViews:(NSArray *)views +- (CPString)formattingKeyForViews:(CPArray)views { var keyParts = []; var count = [views count]; @@ -133,7 +133,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } // Localizes all popup menu options within the row under their proper context -- (void)localizeMenuItemsForViews:(NSArray *)views +- (void)localizeMenuItemsForViews:(CPArray)views { var count = [views count]; for (var i = 0; i < count; i++) @@ -189,7 +189,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } // Translates labels and reorders the active subviews based on positional formatting string -- (NSArray *)localizeAndReorderViews:(NSArray *)views +- (CPArray)localizeAndReorderViews:(CPArray)views { var key = [self formattingKeyForViews:views]; var localizedPattern = [self localizedStringForString:key]; From e856605a0991a6156c9c821765c89fcf10fcbe4d Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 28 May 2026 17:38:14 +0200 Subject: [PATCH 3/8] _CPRuleEditorViewSliceRow integration --- AppKit/CPRuleEditor/CPRuleEditor.j | 26 +++++++++++++++---- .../CPRuleEditor/_CPRuleEditorViewSliceRow.j | 22 ++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 1910d6153..b1f30dc30 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -127,7 +127,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", BOOL _isKeyDown; BOOL _nestingModeDidChange; - _CPRuleEditorLocalizer _standardLocalizer @accessors(property=standardLocalizer); + _CPRuleEditorLocalizer _standardLocalizer; CPDictionary _itemsAndValuesToAddForRowType; } @@ -384,7 +384,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", */ - (CPDictionary)formattingDictionary { - return [_standardLocalizer dictionary]; + return [[self standardLocalizer] dictionary]; } /*! @@ -396,6 +396,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", */ - (void)setFormattingDictionary:(CPDictionary)dictionary { + if (_standardLocalizer == nil) + _standardLocalizer = [_CPRuleEditorLocalizer new]; + [_standardLocalizer setDictionary:dictionary]; _stringsFilename = nil; } @@ -440,6 +443,19 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", } } +- (_CPRuleEditorLocalizer)standardLocalizer +{ + if (_standardLocalizer == nil) + _standardLocalizer = [_CPRuleEditorLocalizer new]; + + return _standardLocalizer; +} + +- (void)setStandardLocalizer:(_CPRuleEditorLocalizer)aLocalizer +{ + _standardLocalizer = aLocalizer; +} + /*! @name Providing Data */ @@ -1924,17 +1940,17 @@ TODO: implement - (CPString)_toolTipForAddCompoundRowButton { - return [_standardLocalizer localizedStringForString:@"Add compound row"]; + return [[self standardLocalizer] localizedStringForString:@"Add compound row"]; } - (CPString)_toolTipForAddSimpleRowButton { - return [_standardLocalizer localizedStringForString:@"Add row"]; + return [[self standardLocalizer] localizedStringForString:@"Add row"]; } - (CPString)_toolTipForDeleteRowButton { - return [_standardLocalizer localizedStringForString:@"Delete row"]; + return [[self standardLocalizer] localizedStringForString:@"Delete row"]; } - (void)_updateSliceIndentations diff --git a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j index 8866095d4..181f935ba 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j @@ -334,6 +334,28 @@ [_correspondingRuleItems setArray:ruleItems]; + // Localize drop-down options in context and reorder/insert intermediate labels natively + var localizer = [_ruleEditor standardLocalizer]; + if (localizer) + { + [localizer localizeMenuItemsForViews:_ruleOptionViews]; + _ruleOptionViews = [localizer localizeAndReorderViews:_ruleOptionViews]; + } + + // Rebuild frame configurations to match the new localized layout order + [_ruleOptionFrames removeAllObjects]; + [_ruleOptionInitialViewFrames removeAllObjects]; + + var newCount = [_ruleOptionViews count]; + for (var i = 0; i < newCount; i++) + { + var view = [_ruleOptionViews objectAtIndex:i], + frame = [view frame]; + + [_ruleOptionFrames addObject:frame]; + [_ruleOptionInitialViewFrames addObject:frame]; + } + if (!_editable) [self _updateEnabledStateForSubviews]; From 5e0fc1cbb549daf81c7fce461cd69a096a0938b3 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Tue, 2 Jun 2026 22:20:44 +0200 Subject: [PATCH 4/8] fixes + manual test --- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 51 +++-- .../CPRuleEditor/_CPRuleEditorViewSliceRow.j | 44 ++-- .../CPRuleEditorTestSpanish/AppController.j | 92 ++++++++ .../Manual/CPRuleEditorTestSpanish/Info.plist | 12 ++ Tests/Manual/CPRuleEditorTestSpanish/Jakefile | 94 ++++++++ .../Resources/Spanish.strings | 13 ++ .../Resources/spinner.gif | Bin 0 -> 1434 bytes .../CPRuleEditorTestSpanish/RuleDelegate.j | 113 ++++++++++ .../CPRuleEditorTestSpanish/index-debug.html | 204 ++++++++++++++++++ .../Manual/CPRuleEditorTestSpanish/index.html | 166 ++++++++++++++ Tests/Manual/CPRuleEditorTestSpanish/main.j | 18 ++ 11 files changed, 771 insertions(+), 36 deletions(-) create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/AppController.j create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Info.plist create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Jakefile create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/index-debug.html create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/index.html create mode 100644 Tests/Manual/CPRuleEditorTestSpanish/main.j diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index db08bd739..f9dddbfb9 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -94,9 +94,8 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) return aString; } -#pragma mark - Whole Sentence Formatting & Reordering Helpers +#pragma mark - Formatting & Reordering Helpers -// Constructs the English-matching format representation of a single view - (CPString)_englishRepresentationForView:(id)aView { if ([aView isKindOfClass:[CPPopUpButton class]]) @@ -104,14 +103,32 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) var selectedItem = [aView selectedItem]; if (selectedItem) { - var originalTitle = [selectedItem representedObject] || selectedItem._originalTitle || [selectedItem title]; + var originalTitle = selectedItem._originalTitle; + + // Fallback: If not cached directly, inspect representedObject payload dictionary + if (!originalTitle) + { + var rep = [selectedItem representedObject]; + if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + { + originalTitle = [rep objectForKey:@"value"]; + } + else if (rep && typeof rep === "string") + { + originalTitle = rep; + } + } + if (!originalTitle) + { + originalTitle = [selectedItem title]; + } return "%[" + originalTitle + "]@"; } return "%[]@"; } else if ([aView isKindOfClass:[CPTextField class]] && ![aView isEditable]) { - return [aView stringValue]; + return aView._originalText || [aView stringValue]; } else { @@ -119,7 +136,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } -// Builds the current formatting lookup key (e.g. "%[property]@ %[is]@ %@") - (CPString)formattingKeyForViews:(CPArray)views { var keyParts = []; @@ -132,7 +148,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) return [keyParts componentsJoinedByString:@" "]; } -// Localizes all popup menu options within the row under their proper context - (void)localizeMenuItemsForViews:(CPArray)views { var count = [views count]; @@ -151,10 +166,18 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (!item._originalTitle) { - item._originalTitle = [item title]; + var rep = [item representedObject]; + if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + { + item._originalTitle = [rep objectForKey:@"value"]; + } + else + { + item._originalTitle = [item title]; + } } - // Temporarily select item to formulate the unique localization key + // Temporarily select item to generate formatting key context [view selectItem:item]; var tempKey = [self formattingKeyForViews:views]; @@ -162,7 +185,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (tempPattern !== tempKey) { - // Search for this view position's translation inside the pattern (e.g. %2$[son iguales]@) var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; var match; while ((match = regex.exec(tempPattern)) !== null) @@ -179,7 +201,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } - // Restore initial selection if (selectedItem) { [view selectItem:selectedItem]; @@ -188,7 +209,6 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } -// Translates labels and reorders the active subviews based on positional formatting string - (CPArray)localizeAndReorderViews:(CPArray)views { var key = [self formattingKeyForViews:views]; @@ -200,23 +220,19 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } var newViews = [CPMutableArray array]; - - // Pattern to extract index and translation: %1$[translation]@ or %1$@ var regex = /%(\d+)\$(?:\[([^\]]+)\])?@/g; var lastIndex = 0; var match; while ((match = regex.exec(localizedPattern)) !== null) { - // 1. Insert any leading static text (e.g. " y ") var literalText = localizedPattern.substring(lastIndex, match.index); if (literalText.length > 0) { - var label = [CPTextField labelWithString:literalText]; + var label = [CPTextField labelWithTitle:literalText]; [newViews addObject:label]; } - // 2. Identify the original view corresponding to the positional index var position = parseInt(match[1], 10) - 1; var translatedValue = match[2]; @@ -250,13 +266,12 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) lastIndex = regex.lastIndex; } - // 3. Append trailing static text if (lastIndex < localizedPattern.length) { var literalText = localizedPattern.substring(lastIndex); if (literalText.length > 0) { - var label = [CPTextField labelWithString:literalText]; + var label = [CPTextField labelWithTitle:literalText]; [newViews addObject:label]; } } diff --git a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j index 181f935ba..a32764fbe 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j @@ -114,8 +114,32 @@ - (CPMenuItem)_createMenuItemWithTitle:(CPString )title { - title = [[_ruleEditor standardLocalizer] localizedStringForString:title]; - return [[CPMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""]; + var originalTitle = title; + var localizedTitle = [[_ruleEditor standardLocalizer] localizedStringForString:title]; + var item = [[CPMenuItem alloc] initWithTitle:localizedTitle action:nil keyEquivalent:@""]; + + // Cache the raw English title for pattern-matching + item._originalTitle = originalTitle; + return item; +} + +- (CPTextField)_createStaticTextFieldWithStringValue:(CPString)text +{ + var textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()], + ruleEditorFont = [_ruleEditor font], + font = [CPFont fontWithName:[ruleEditorFont familyName] size:[ruleEditorFont size] + 2], + localizedText = [[_ruleEditor standardLocalizer] localizedStringForString:text], + size = [localizedText sizeWithFont:font]; + + [textField setFrameSize:CGSizeMake(size.width + 4, [_ruleEditor rowHeight])]; + [textField setValue:font forThemeAttribute:@"font"]; + [textField setValue:[_ruleEditor _verticalAlignment] forThemeAttribute:@"vertical-alignment"]; + [textField setStringValue:localizedText]; + + // Cache the raw English text for pattern-matching + textField._originalText = text; + + return textField; } - (CPPopUpButton)_createPopUpButtonWithItems:(CPArray)itemsArray selectedItemIndex:(int)index @@ -142,22 +166,6 @@ return [CPMenuItem separatorItem]; } -- (CPTextField)_createStaticTextFieldWithStringValue:(CPString)text -{ - var textField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()], - ruleEditorFont = [_ruleEditor font], - font = [CPFont fontWithName:[ruleEditorFont familyName] size:[ruleEditorFont size] + 2], - localizedText = [[_ruleEditor standardLocalizer] localizedStringForString:text], - size = [localizedText sizeWithFont:font]; - - [textField setFrameSize:CGSizeMake(size.width + 4, [_ruleEditor rowHeight])]; - [textField setValue:font forThemeAttribute:@"font"]; - [textField setValue:[_ruleEditor _verticalAlignment] forThemeAttribute:@"vertical-alignment"]; - [textField setStringValue:localizedText]; - - return textField; -} - - (void)_addOption:(id)sender { if (_rowIndex == [_ruleEditor numberOfRows] - 1) diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j new file mode 100644 index 000000000..e81076955 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j @@ -0,0 +1,92 @@ +@import +@import +@import +@import +@import +@import "RuleDelegate.j" + +@implementation AppController : CPObject +{ + CPWindow theWindow; + CPRuleEditor ruleEditor; + CPTextField predicateField; + RuleDelegate ruleDelegate; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(50, 50, 800, 500) + styleMask:CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask]; + [theWindow setTitle:@"Spanish CPRuleEditor Whole-Sentence Localization Test"]; + [theWindow setFullPlatformWindow:YES]; + + var contentView = [theWindow contentView]; + [contentView setBackgroundColor:[CPColor colorWithHexString:@"f3f4f5"]]; + + var label = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"]; + [label setFrame:CGRectMake(20, 20, 760, 24)]; + [label setFont:[CPFont boldSystemFontOfSize:14]]; + [contentView addSubview:label]; + + ruleDelegate = [[RuleDelegate alloc] init]; + + // Create Rule Editor + ruleEditor = [[CPRuleEditor alloc] initWithFrame:CGRectMake(20, 55, 760, 250)]; + [ruleEditor setAutoresizingMask:CPViewWidthSizable]; + [ruleEditor setDelegate:ruleDelegate]; + [ruleEditor setEditable:YES]; + [ruleEditor setRowHeight:28]; + [ruleEditor setTarget:self]; + [ruleEditor setAction:@selector(ruleEditorAction:)]; + + // Populate Spanish translations programmatically + var path = [[CPBundle mainBundle] pathForResource:@"Spanish.strings"]; + if (path) + { + [[ruleEditor standardLocalizer] loadContentOfURL:[CPURL URLWithString:path]]; + } + + [contentView addSubview:ruleEditor]; + + // Populate default row + [ruleEditor addRow:self]; + + // Display Output Label + var predLabel = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"]; + [predLabel setFrame:CGRectMake(20, 320, 760, 20)]; + [predLabel setFont:[CPFont boldSystemFontOfSize:12]]; + [contentView addSubview:predLabel]; + + // Predicate string value display + predicateField = [[CPTextField alloc] initWithFrame:CGRectMake(20, 345, 760, 36)]; + [predicateField setAutoresizingMask:CPViewWidthSizable]; + [predicateField setBezeled:YES]; + [predicateField setEditable:NO]; + [predicateField setStringValue:@""]; + [predicateField setFont:[CPFont systemFontOfSize:13]]; + [contentView addSubview:predicateField]; + + var addBtn = [CPButton buttonWithTitle:@"Añadir regla"]; + [addBtn setFrame:CGRectMake(20, 400, 120, 24)]; + [addBtn setTarget:ruleEditor]; + [addBtn setAction:@selector(addRow:)]; + [contentView addSubview:addBtn]; + + [theWindow orderFront:self]; + [self ruleEditorAction:nil]; +} + +- (void)ruleEditorAction:(id)sender +{ + var predicate = [ruleEditor predicate]; + if (predicate) + { + [predicateField setStringValue:[predicate predicateFormat]]; + } + else + { + [predicateField setStringValue:@"(No predicate evaluated)"]; + } +} + +@end diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Info.plist b/Tests/Manual/CPRuleEditorTestSpanish/Info.plist new file mode 100644 index 000000000..68f9e7d32 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/Info.plist @@ -0,0 +1,12 @@ + + + + + CPApplicationDelegateClass + AppController + CPBundleName + CPTextViewTest + CPPrincipalClass + CPApplication + + diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Jakefile b/Tests/Manual/CPRuleEditorTestSpanish/Jakefile new file mode 100644 index 000000000..c66879a92 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/Jakefile @@ -0,0 +1,94 @@ +/* + * Jakefile + * CPRuleEditorCibTest + * + * Created by You on September 3, 2010. + * Copyright 2010, Your Company All rights reserved. + */ + +var ENV = require("system").env, + FILE = require("file"), + JAKE = require("jake"), + task = JAKE.task, + FileList = JAKE.FileList, + app = require("cappuccino/jake").app, + configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug", + OS = require("os"); + +app ("CPRuleEditorCibTest", function(task) +{ + task.setBuildIntermediatesPath(FILE.join("Build", "CPRuleEditorCibTest.build", configuration)); + task.setBuildPath(FILE.join("Build", configuration)); + + task.setProductName("CPRuleEditorCibTest"); + task.setIdentifier("com.yourcompany.CPRuleEditorCibTest"); + task.setVersion("1.0"); + task.setAuthor("Your Company"); + task.setEmail("feedback @nospam@ yourcompany.com"); + task.setSummary("CPRuleEditorCibTest"); + task.setSources((new FileList("**/*.j")).exclude(FILE.join("Build", "**"))); + task.setResources(new FileList("Resources/**")); + task.setIndexFilePath("index.html"); + task.setInfoPlistPath("Info.plist"); + task.setNib2CibFlags("-R Resources/"); + + if (configuration === "Debug") + task.setCompilerFlags("-DDEBUG -g"); + else + task.setCompilerFlags("-O"); +}); + +task ("default", ["CPRuleEditorCibTest"], function() +{ + printResults(configuration); +}); + +task ("build", ["default"]); + +task ("debug", function() +{ + ENV["CONFIGURATION"] = "Debug"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("release", function() +{ + ENV["CONFIGURATION"] = "Release"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("run", ["debug"], function() +{ + OS.system(["open", FILE.join("Build", "Debug", "CPRuleEditorCibTest", "index.html")]); +}); + +task ("run-release", ["release"], function() +{ + OS.system(["open", FILE.join("Build", "Release", "CPRuleEditorCibTest", "index.html")]); +}); + +task ("deploy", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Deployment", "CPRuleEditorCibTest")); + OS.system(["press", "-f", FILE.join("Build", "Release", "CPRuleEditorCibTest"), FILE.join("Build", "Deployment", "CPRuleEditorCibTest")]); + printResults("Deployment") +}); + +task ("desktop", ["release"], function() +{ + FILE.mkdirs(FILE.join("Build", "Desktop", "CPRuleEditorCibTest")); + require("cappuccino/nativehost").buildNativeHost(FILE.join("Build", "Release", "CPRuleEditorCibTest"), FILE.join("Build", "Desktop", "CPRuleEditorCibTest", "CPRuleEditorCibTest.app")); + printResults("Desktop") +}); + +task ("run-desktop", ["desktop"], function() +{ + OS.system([FILE.join("Build", "Desktop", "CPRuleEditorCibTest", "CPRuleEditorCibTest.app", "Contents", "MacOS", "NativeHost"), "-i"]); +}); + +function printResults(configuration) +{ + print("----------------------------"); + print(configuration+" app built at path: "+FILE.join("Build", configuration, "CPRuleEditorCibTest")); + print("----------------------------"); +} diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings new file mode 100644 index 000000000..17a1738c9 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings @@ -0,0 +1,13 @@ +/* Spanish formatting patterns for CPRuleEditor */ + +"%[firstName]@ %[is equal to]@ %@" = "%1$[Nombre]@ y %3$@ %2$[son iguales]@"; +"%[firstName]@ %[contains]@ %@" = "%1$[Nombre]@ %2$[contiene]@ %3$@"; + +"%[lastName]@ %[is equal to]@ %@" = "%1$[Apellido]@ y %3$@ %2$[son iguales]@"; +"%[lastName]@ %[contains]@ %@" = "%1$[Apellido]@ %2$[contiene]@ %3$@"; + +"%[age]@ %[is equal to]@ %@" = "%1$[Edad]@ y %3$@ %2$[son iguales]@"; + +"Add row" = "Añadir regla"; +"Delete row" = "Eliminar regla"; +"Add compound row" = "Añadir grupo de reglas"; \ No newline at end of file diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif b/Tests/Manual/CPRuleEditorTestSpanish/Resources/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..a5e705f6cbdf914e5e714c35a8dfef807f19e3c2 GIT binary patch literal 1434 zcmZvbdrVVj7{t$-UNOu86#VHu^|s&3rWfwHBbPG_8{SrlB{Tv1uvvj4t6zP!)#d!Ogc zP^62J^$0+~?-ZcXXpBaq%jFsw8L`=H2?+@R02Yg-*Xw;gACBV^i3CMahr>Z667S!? zk3LDe>VLEsU;sWo$Py~o!gWuaIAnaG3Sv``&tvc+8DJO!| zt4fcbQ8tW}a&xZfgtQ9BnFYLvGG|PvCNlg&uh@Q^w9Pz}+I^hCj`vu9JQADPqdkyk zR40xycD9geUgJu1e;$L`Fpa)?Wl-2&6hO@U*KrPqK(I1H=1h?2fce}6GhhP1oBZC# z1e@gt-qFa6lZrl%s1>bdxg*W)Ebt__O(4sj6(*2X@ciUClwHH#U(NRM7XAjS z+scDZ32J*PCoslsgS~#ZlCCGo`r>S6F)Ghw5wVlqHioFaw?ucD(XoLJ0j;28oPoPV z9A}dR$0SKn>uExfkl#j09o;v$BZ909R=*p{ATNq8BJW;YduX2QMOU7$a$_L5e!G^g zpbkx5G4&FZ%7m14rTN}5YB(;x7$5XOc_hf3E|Hw$TVg)P#qf~}nOn03uqsp>UG>vi zWThIoO)-1Q#@u#O;fMC#WAf@Xj70-0g9YZ;dA$HH1fW1q=9-c>>_yA0S$7M*5?}vi z)8MU`Q%P!7sEBBnd$DrKgFQ2?O%6LpdaC%F8Pn-)EC2t=j~ zoaLamla$FX!GQqWi!%s>sj-#5SJX0IF!TP=r21Z}s)k#btN0?=I7uD9C)Gb$fZ#sm zaVq7g`LwZ%PfNpN^_N-IGLHj@AV`s#4&va7_{Hw63G6BkSGXHdogTZ%QOiRb bDpZ@qYcJKM>x%b%*HX74c8MnqfHi*u-bC?g literal 0 HcmV?d00001 diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j new file mode 100644 index 000000000..1c536b7c6 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j @@ -0,0 +1,113 @@ +@import +@import +@import + +@implementation RuleDelegate : CPObject +{ +} + +// 1. Root criteria and children +- (id)ruleEditor:(CPRuleEditor)editor child:(CPInteger)index forCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType +{ + if (criterion == nil) + { + // Root criteria + return [@[@"firstName", @"lastName", @"age"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) + { + return [@[@"contains", @"is equal to"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"age"]) + { + return [@[@"is equal to"] objectAtIndex:index]; + } + + if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) + { + // The child of an operator is the leaf value node + return @"value"; + } + + return nil; +} + +// 2. Number of children for a given criterion +- (CPInteger)ruleEditor:(CPRuleEditor)editor numberOfChildrenForCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType +{ + if (criterion == nil) + { + return 3; // firstName, lastName, age + } + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) + { + return 2; // contains, is equal to + } + + if ([criterion isEqualToString:@"age"]) + { + return 1; // is equal to + } + + if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) + { + // Operators have one child representing the input field value + return 1; + } + + return 0; // Leaf nodes return 0 +} + +// 3. Display values (labels, popup titles, or text input fields) +- (id)ruleEditor:(CPRuleEditor)editor displayValueForCriterion:(id)criterion inRow:(CPInteger)row +{ + if ([criterion isEqualToString:@"firstName"]) return @"firstName"; + if ([criterion isEqualToString:@"lastName"]) return @"lastName"; + if ([criterion isEqualToString:@"age"]) return @"age"; + + if ([criterion isEqualToString:@"contains"]) return @"contains"; + if ([criterion isEqualToString:@"is equal to"]) return @"is equal to"; + + if ([criterion isEqualToString:@"value"]) + { + // Return the actual editable text field view for the leaf node + var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 120, 24)]; + [textField setBezeled:YES]; + [textField setBezelStyle:CPTextFieldSquareBezel]; + [textField setEditable:YES]; + [textField setStringValue:@""]; + return textField; + } + + return nil; +} + +// 4. Predicate parts mapping +- (CPDictionary)ruleEditor:(CPRuleEditor)editor predicatePartsForCriterion:(id)criterion withDisplayValue:(id)value inRow:(CPInteger)row +{ + var parts = @{}; + + if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"] || [criterion isEqualToString:@"age"]) + { + [parts setObject:[CPExpression expressionForKeyPath:criterion] forKey:CPRuleEditorPredicateLeftExpression]; + } + else if ([criterion isEqualToString:@"contains"]) + { + [parts setObject:[CPNumber numberWithUnsignedInt:CPContainsPredicateOperatorType] forKey:CPRuleEditorPredicateOperatorType]; + } + else if ([criterion isEqualToString:@"is equal to"]) + { + [parts setObject:[CPNumber numberWithUnsignedInt:CPEqualToPredicateOperatorType] forKey:CPRuleEditorPredicateOperatorType]; + } + else if ([criterion isEqualToString:@"value"]) + { + [parts setObject:[CPExpression expressionForConstantValue:[value stringValue]] forKey:CPRuleEditorPredicateRightExpression]; + } + + return parts; +} + +@end diff --git a/Tests/Manual/CPRuleEditorTestSpanish/index-debug.html b/Tests/Manual/CPRuleEditorTestSpanish/index-debug.html new file mode 100644 index 000000000..a36b1d3b9 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/index-debug.html @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPRuleEditorTestSpanish/index.html b/Tests/Manual/CPRuleEditorTestSpanish/index.html new file mode 100644 index 000000000..ac42c98a7 --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/CPRuleEditorTestSpanish/main.j b/Tests/Manual/CPRuleEditorTestSpanish/main.j new file mode 100644 index 000000000..7424228fa --- /dev/null +++ b/Tests/Manual/CPRuleEditorTestSpanish/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * CPRuleEditorCibTest + * + * Created by You on September 3, 2010. + * Copyright 2010, Your Company All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} From 750c123d1e5787fd6bb1a3baa9a497416bf5a223 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 3 Jun 2026 19:19:36 +0200 Subject: [PATCH 5/8] fixed: redraw issues --- AppKit/CPRuleEditor/CPRuleEditor.j | 20 +++++++++++++ AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 3 ++ .../CPRuleEditorTestSpanish/RuleDelegate.j | 30 +++++++++++++++++-- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index b1f30dc30..3ac9d2b80 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -207,8 +207,28 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", [self registerForDraggedTypes:[CPArray arrayWithObjects:CPRuleEditorItemPBoardType,nil]]; [_boundArrayOwner addObserver:self forKeyPath:_boundArrayKeyPath options:CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:boundArrayContext]; + + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(_ruleEditorLocalizerDidLoad:) + name:@"_CPRuleEditorLocalizerDidLoadNotification" + object:nil]; } +- (void)_ruleEditorLocalizerDidLoad:(CPNotification)aNotification +{ + if ([aNotification object] === [self standardLocalizer]) + { + // Defer execution to the next run loop cycle so that any active slice + // insertions have fully completed and are present in the `_slices` array. + [[CPRunLoop mainRunLoop] performBlock:function() { + var count = [_slices count]; + for (var i = 0; i < count; i++) + { + [[_slices objectAtIndex:i] _reconfigureSubviews]; + } + } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; + } +} /*! @endcond */ /*! diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index f9dddbfb9..79ab94da8 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -77,6 +77,9 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } _dictionary = [CPDictionary dictionaryWithDictionary:dict]; + + // Post notification to let the rule editor know the translation dictionary is ready + [[CPNotificationCenter defaultCenter] postNotificationName:@"_CPRuleEditorLocalizerDidLoadNotification" object:self]; } - (CPString)localizedStringForString:(CPString)aString diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j index 1c536b7c6..842056535 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j @@ -17,7 +17,8 @@ if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) { - return [@[@"contains", @"is equal to"] objectAtIndex:index]; + // "is equal to" is placed at index 0 so that the reordered Spanish sentence layout loads automatically on startup + return [@[@"is equal to", @"contains"] objectAtIndex:index]; } if ([criterion isEqualToString:@"age"]) @@ -54,7 +55,7 @@ if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) { - // Operators have one child representing the input field value + // Operators have 1 child representing the value node return 1; } @@ -104,7 +105,30 @@ } else if ([criterion isEqualToString:@"value"]) { - [parts setObject:[CPExpression expressionForConstantValue:[value stringValue]] forKey:CPRuleEditorPredicateRightExpression]; + // Resolve the correct active text field from the slice row on screen + var activeValue = value; + var slices = [editor valueForKey:@"_slices"]; + + if (slices && row < [slices count]) + { + var slice = [slices objectAtIndex:row]; + var optionViews = [slice valueForKey:@"_ruleOptionViews"]; + if (optionViews) + { + var count = [optionViews count]; + for (var i = 0; i < count; i++) + { + var view = [optionViews objectAtIndex:i]; + if ([view isKindOfClass:[CPTextField class]] && [view isEditable]) + { + activeValue = view; + break; + } + } + } + } + + [parts setObject:[CPExpression expressionForConstantValue:[activeValue stringValue]] forKey:CPRuleEditorPredicateRightExpression]; } return parts; From e9b9d9702e6249f4f4f64136cbab88ee7992b95a Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 3 Jun 2026 21:19:09 +0200 Subject: [PATCH 6/8] fixed: localizer issues --- AppKit/CPRuleEditor/CPRuleEditor.j | 32 +++++++++++++++---- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 22 ++++++++++--- .../CPRuleEditorTestSpanish/AppController.j | 21 ++++++++++++ .../Resources/Spanish.strings | 3 +- .../CPRuleEditorTestSpanish/RuleDelegate.j | 24 +++++++------- 5 files changed, 77 insertions(+), 25 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 3ac9d2b80..cbc08326f 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -226,6 +226,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", { [[_slices objectAtIndex:i] _reconfigureSubviews]; } + + [self _updatePredicate]; + [self _sendRuleAction]; } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; } } @@ -574,7 +577,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", if ([self rowTypeForRow:current_index] === CPRuleEditorRowTypeCompound) { var candidate = [[self _rowCacheForIndex:current_index] rowObject], - subObjects = [[self _subrowObjectsOfObject:candidate] _representedObject]; + subObjects = [self _subrowObjectsOfObject:candidate]; // Standard direct array query if ([subObjects indexOfObjectIdenticalTo:targetObject] !== CPNotFound) return current_index; @@ -641,7 +644,7 @@ TODO: implement for (var i = rowIndex + 1; i < count; i++) { var candidate = [[self _rowCacheForIndex:i] rowObject], - indexInSubrows = [[subobjects _representedObject] indexOfObjectIdenticalTo:candidate]; + indexInSubrows = [subobjects indexOfObjectIdenticalTo:candidate]; // Standard direct array query if (indexInSubrows !== CPNotFound) { @@ -810,7 +813,7 @@ TODO: implement while (current_index !== CPNotFound) { var rowObject = [[self _rowCacheForIndex:current_index] rowObject], - relativeChildIndex = [[subrows _representedObject] indexOfObjectIdenticalTo:rowObject]; + relativeChildIndex = [subrows indexOfObjectIdenticalTo:rowObject]; // Standard direct array query if (relativeChildIndex !== CPNotFound) [childsIndexes addIndex:relativeChildIndex]; @@ -867,7 +870,6 @@ TODO: implement for (i = 0; i < count; i++) { var item = [items objectAtIndex:i], - //var displayValue = [self _queryValueForItem:item inRow:aRow]; Ask the delegate or get cached value ?. displayValue = [[self displayValuesForRow:aRow] objectAtIndex:i], predpart = [self _sendDelegateRuleEditorPredicatePartsForCriterion:item withDisplayValue:displayValue inRow:aRow]; @@ -885,6 +887,7 @@ TODO: implement return nil; var current_index = [subrowsIndexes firstIndex]; + while (current_index !== CPNotFound) { var subpredicate = [self predicateForRow:current_index]; @@ -1392,8 +1395,25 @@ TODO: implement if ([self rowTypeForRow:row] === type && itemIndex < [aCriteria count]) { - var crit = [aCriteria objectAtIndex:itemIndex]; - [current_criterions addObject:crit]; + // Verify that this row's parent path matches the path currently being built + var pathMatches = true; + for (var p = 0; p < itemIndex; p++) + { + var criterionA = [aCriteria objectAtIndex:p], + criterionB = [items objectAtIndex:p]; + + if (criterionA !== criterionB && (typeof criterionA.isEqual !== "function" || ![criterionA isEqual:criterionB])) + { + pathMatches = false; + break; + } + } + + if (pathMatches) + { + var crit = [aCriteria objectAtIndex:itemIndex]; + [current_criterions addObject:crit]; + } } } diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index 79ab94da8..467445242 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -112,7 +112,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (!originalTitle) { var rep = [selectedItem representedObject]; - if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)]) { originalTitle = [rep objectForKey:@"value"]; } @@ -170,7 +170,7 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (!item._originalTitle) { var rep = [item representedObject]; - if (rep && typeof rep === "object" && typeof rep.objectForKey === "function") + if (rep && typeof rep === "object" && [rep respondsToSelector:@selector(objectForKey:)]) { item._originalTitle = [rep objectForKey:@"value"]; } @@ -230,7 +230,9 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) while ((match = regex.exec(localizedPattern)) !== null) { var literalText = localizedPattern.substring(lastIndex, match.index); - if (literalText.length > 0) + + // Only add a label if there are actual non-whitespace characters (like 'y') + if (literalText.length > 0 && /\S/.test(literalText)) { var label = [CPTextField labelWithTitle:literalText]; [newViews addObject:label]; @@ -257,9 +259,17 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) [selectedItem setTitle:translatedValue]; } } - else if (typeof originalView.setStringValue === "function") + else if ([originalView respondsToSelector:@selector(setStringValue:)]) { [originalView setStringValue:translatedValue]; + + // Recalculate frame size if it is a static CPTextField to avoid visual clipping + if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable]) + { + var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]], + size = [translatedValue sizeWithFont:font]; + [originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))]; + } } } @@ -272,7 +282,9 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (lastIndex < localizedPattern.length) { var literalText = localizedPattern.substring(lastIndex); - if (literalText.length > 0) + + // Only add a label if there are actual non-whitespace characters + if (literalText.length > 0 && /\S/.test(literalText)) { var label = [CPTextField labelWithTitle:literalText]; [newViews addObject:label]; diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j index e81076955..d39cbf981 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j @@ -35,6 +35,7 @@ [ruleEditor setAutoresizingMask:CPViewWidthSizable]; [ruleEditor setDelegate:ruleDelegate]; [ruleEditor setEditable:YES]; + [ruleEditor setNestingMode:CPRuleEditorNestingModeList]; [ruleEditor setRowHeight:28]; [ruleEditor setTarget:self]; [ruleEditor setAction:@selector(ruleEditorAction:)]; @@ -72,6 +73,12 @@ [addBtn setAction:@selector(addRow:)]; [contentView addSubview:addBtn]; + + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(ruleEditorRowsDidChange:) + name:CPRuleEditorRowsDidChangeNotification + object:ruleEditor]; + [theWindow orderFront:self]; [self ruleEditorAction:nil]; } @@ -89,4 +96,18 @@ } } +- (void)ruleEditorRowsDidChange:(CPNotification)note +{ + var predicate = [ruleEditor predicate]; + + if (predicate) + { + [predicateField setStringValue:[predicate predicateFormat]]; + } + else + { + [predicateField setStringValue:@"(Incomplete Predicate)"]; + } +} + @end diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings index 17a1738c9..75dd5e735 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings +++ b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings @@ -7,7 +7,8 @@ "%[lastName]@ %[contains]@ %@" = "%1$[Apellido]@ %2$[contiene]@ %3$@"; "%[age]@ %[is equal to]@ %@" = "%1$[Edad]@ y %3$@ %2$[son iguales]@"; +"%[age]@ is equal to %@" = "%1$[Edad]@ y %3$@ %2$[son iguales]@"; "Add row" = "Añadir regla"; "Delete row" = "Eliminar regla"; -"Add compound row" = "Añadir grupo de reglas"; \ No newline at end of file +"Add compound row" = "Añadir grupo de reglas"; diff --git a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j index 842056535..3835b5e7f 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/RuleDelegate.j @@ -2,6 +2,10 @@ @import @import +// Ensure the standard operator constants are explicitly defined +var CPEqualToPredicateOperatorType = 4, + CPContainsPredicateOperatorType = 99; + @implementation RuleDelegate : CPObject { } @@ -11,13 +15,11 @@ { if (criterion == nil) { - // Root criteria return [@[@"firstName", @"lastName", @"age"] objectAtIndex:index]; } if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) { - // "is equal to" is placed at index 0 so that the reordered Spanish sentence layout loads automatically on startup return [@[@"is equal to", @"contains"] objectAtIndex:index]; } @@ -28,41 +30,39 @@ if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) { - // The child of an operator is the leaf value node return @"value"; } return nil; } -// 2. Number of children for a given criterion +// 2. Number of children - (CPInteger)ruleEditor:(CPRuleEditor)editor numberOfChildrenForCriterion:(id)criterion withRowType:(CPRuleEditorRowType)rowType { if (criterion == nil) { - return 3; // firstName, lastName, age + return 3; } if ([criterion isEqualToString:@"firstName"] || [criterion isEqualToString:@"lastName"]) { - return 2; // contains, is equal to + return 2; } if ([criterion isEqualToString:@"age"]) { - return 1; // is equal to + return 1; } if ([criterion isEqualToString:@"contains"] || [criterion isEqualToString:@"is equal to"]) { - // Operators have 1 child representing the value node return 1; } - return 0; // Leaf nodes return 0 + return 0; } -// 3. Display values (labels, popup titles, or text input fields) +// 3. Display values - (id)ruleEditor:(CPRuleEditor)editor displayValueForCriterion:(id)criterion inRow:(CPInteger)row { if ([criterion isEqualToString:@"firstName"]) return @"firstName"; @@ -74,7 +74,6 @@ if ([criterion isEqualToString:@"value"]) { - // Return the actual editable text field view for the leaf node var textField = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 120, 24)]; [textField setBezeled:YES]; [textField setBezelStyle:CPTextFieldSquareBezel]; @@ -86,7 +85,7 @@ return nil; } -// 4. Predicate parts mapping +// 4. Predicate parts - (CPDictionary)ruleEditor:(CPRuleEditor)editor predicatePartsForCriterion:(id)criterion withDisplayValue:(id)value inRow:(CPInteger)row { var parts = @{}; @@ -105,7 +104,6 @@ } else if ([criterion isEqualToString:@"value"]) { - // Resolve the correct active text field from the slice row on screen var activeValue = value; var slices = [editor valueForKey:@"_slices"]; From 613a2722be1d618fbffdf5599f0081422ff12ae2 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 4 Jun 2026 08:15:52 +0200 Subject: [PATCH 7/8] new: dynamic mutilanguage demo --- AppKit/CPRuleEditor/CPRuleEditor.j | 5 +- AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j | 30 +++++ .../CPRuleEditor/_CPRuleEditorViewSliceRow.j | 3 + .../CPRuleEditorTestSpanish/AppController.j | 104 ++++++++++++++---- .../Resources/Spanish.strings | 14 --- 5 files changed, 119 insertions(+), 37 deletions(-) delete mode 100644 Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index cbc08326f..9e5a2cbdd 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -214,6 +214,7 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", object:nil]; } + - (void)_ruleEditorLocalizerDidLoad:(CPNotification)aNotification { if ([aNotification object] === [self standardLocalizer]) @@ -224,7 +225,9 @@ var CPRuleEditorItemPBoardType = @"CPRuleEditorItemPBoardType", var count = [_slices count]; for (var i = 0; i < count; i++) { - [[_slices objectAtIndex:i] _reconfigureSubviews]; + var slice = [_slices objectAtIndex:i]; + [slice _reconfigureSubviews]; + [slice _updateButtonVisibilities]; // Force updates on row button tooltips } [self _updatePredicate]; diff --git a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j index 467445242..14404e0ed 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorLocalizer.j @@ -202,6 +202,10 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) } } } + else + { + [item setTitle:item._originalTitle]; + } } if (selectedItem) @@ -219,6 +223,32 @@ var LocalizerStringsRegex = new RegExp("\"(.+)\"\\s*=\\s*\"(.+)\"\\s*;\\s*(//.+) if (localizedPattern === key) { + var count = [views count]; + + for (var i = 0; i < count; i++) + { + var originalView = [views objectAtIndex:i]; + if ([originalView isKindOfClass:[CPPopUpButton class]]) + { + var selectedItem = [originalView selectedItem]; + if (selectedItem && selectedItem._originalTitle) + { + [selectedItem setTitle:selectedItem._originalTitle]; + } + } + else if ([originalView respondsToSelector:@selector(setStringValue:)] && originalView._originalText) + { + [originalView setStringValue:originalView._originalText]; + + if ([originalView isKindOfClass:[CPTextField class]] && ![originalView isEditable]) + { + var font = [originalView font] || [CPFont systemFontOfSize:[CPFont systemFontSize]], + size = [originalView._originalText sizeWithFont:font]; + [originalView setFrameSize:CGSizeMake(size.width + 4, CGRectGetHeight([originalView frame]))]; + } + } + } + return views; } diff --git a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j index a32764fbe..ee21be432 100644 --- a/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j +++ b/AppKit/CPRuleEditor/_CPRuleEditorViewSliceRow.j @@ -440,6 +440,9 @@ { [_addButton setHidden:[_ruleEditor _shouldHideAddButtonForSlice:self]]; [_subtractButton setHidden:[_ruleEditor _shouldHideSubtractButtonForSlice:self]]; + + [_addButton setToolTip:[_ruleEditor _toolTipForAddSimpleRowButton]]; + [_subtractButton setToolTip:[_ruleEditor _toolTipForDeleteRowButton]]; } - (void)_configurePlusButtonByRowType:(CPRuleEditorRowType)type diff --git a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j index d39cbf981..20c22eeea 100644 --- a/Tests/Manual/CPRuleEditorTestSpanish/AppController.j +++ b/Tests/Manual/CPRuleEditorTestSpanish/AppController.j @@ -3,31 +3,53 @@ @import @import @import +@import @import "RuleDelegate.j" @implementation AppController : CPObject { - CPWindow theWindow; - CPRuleEditor ruleEditor; - CPTextField predicateField; - RuleDelegate ruleDelegate; + CPWindow theWindow; + CPRuleEditor ruleEditor; + CPTextField predicateField; + RuleDelegate ruleDelegate; + CPPopUpButton langPopUp; + + CPDictionary englishDict; + CPDictionary spanishDict; + CPDictionary germanDict; } - (void)applicationDidFinishLaunching:(CPNotification)aNotification { - theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(50, 50, 800, 500) + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(50, 50, 800, 520) styleMask:CPTitledWindowMask | CPClosableWindowMask | CPResizableWindowMask]; - [theWindow setTitle:@"Spanish CPRuleEditor Whole-Sentence Localization Test"]; + [theWindow setTitle:@"CPRuleEditor Multi-Language Localization Demo"]; [theWindow setFullPlatformWindow:YES]; var contentView = [theWindow contentView]; [contentView setBackgroundColor:[CPColor colorWithHexString:@"f3f4f5"]]; var label = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"]; - [label setFrame:CGRectMake(20, 20, 760, 24)]; + [label setFrame:CGRectMake(20, 20, 500, 24)]; [label setFont:[CPFont boldSystemFontOfSize:14]]; [contentView addSubview:label]; + // Language Selector Label + var langLabel = [CPTextField labelWithTitle:@"Language:"]; + [langLabel setFrame:CGRectMake(530, 20, 80, 24)]; + [langLabel setFont:[CPFont boldSystemFontOfSize:12]]; + [langLabel setAlignment:CPRightTextAlignment]; + [contentView addSubview:langLabel]; + + // Language Selector PopUpButton + langPopUp = [[CPPopUpButton alloc] initWithFrame:CGRectMake(620, 16, 160, 24)]; + [langPopUp addItemWithTitle:@"Spanish"]; + [langPopUp addItemWithTitle:@"German"]; + [langPopUp addItemWithTitle:@"English"]; + [langPopUp setTarget:self]; + [langPopUp setAction:@selector(changeLanguage:)]; + [contentView addSubview:langPopUp]; + ruleDelegate = [[RuleDelegate alloc] init]; // Create Rule Editor @@ -40,20 +62,45 @@ [ruleEditor setTarget:self]; [ruleEditor setAction:@selector(ruleEditorAction:)]; - // Populate Spanish translations programmatically - var path = [[CPBundle mainBundle] pathForResource:@"Spanish.strings"]; - if (path) - { - [[ruleEditor standardLocalizer] loadContentOfURL:[CPURL URLWithString:path]]; - } + // Programmatic dictionaries setup + englishDict = [CPDictionary dictionary]; // Identity fallback + + spanishDict = [CPDictionary dictionaryWithDictionary:@{ + @"%[firstName]@ %[is equal to]@ %@" : @"%1$[Nombre]@ y %3$@ %2$[son iguales]@", + @"%[firstName]@ %[contains]@ %@" : @"%1$[Nombre]@ %2$[contiene]@ %3$@", + @"%[lastName]@ %[is equal to]@ %@" : @"%1$[Apellido]@ y %3$@ %2$[son iguales]@", + @"%[lastName]@ %[contains]@ %@" : @"%1$[Apellido]@ %2$[contiene]@ %3$@", + @"%[age]@ %[is equal to]@ %@" : @"%1$[Edad]@ y %3$@ %2$[son iguales]@", + @"%[age]@ is equal to %@" : @"%1$[Edad]@ y %3$@ %2$[son iguales]@", + @"Add row" : @"Añadir regla", + @"Delete row" : @"Eliminar regla", + @"Add compound row" : @"Añadir grupo de reglas" + }]; + + germanDict = [CPDictionary dictionaryWithDictionary:@{ + @"%[firstName]@ %[is equal to]@ %@" : @"%1$[Vorname]@ und %3$@ %2$[sind gleich]@", + @"%[firstName]@ %[contains]@ %@" : @"%1$[Vorname]@ %2$[enthält]@ %3$@", + @"%[lastName]@ %[is equal to]@ %@" : @"%1$[Nachname]@ und %3$@ %2$[sind gleich]@", + @"%[lastName]@ %[contains]@ %@" : @"%1$[Nachname]@ %2$[enthält]@ %3$@", + @"%[age]@ %[is equal to]@ %@" : @"%1$[Alter]@ und %3$@ %2$[sind gleich]@", + @"%[age]@ is equal to %@" : @"%1$[Alter]@ und %3$@ %2$[sind gleich]@", + @"Add row" : @"Regel hinzufügen", + @"Delete row" : @"Regel löschen", + @"Add compound row" : @"Regelgruppe hinzufügen" + }]; + + // Initialize with Spanish by default + [[ruleEditor standardLocalizer] setDictionary:spanishDict]; [contentView addSubview:ruleEditor]; - // Populate default row + // Populate initial rows + [ruleEditor addRow:self]; + [ruleEditor addRow:self]; [ruleEditor addRow:self]; // Display Output Label - var predLabel = [CPTextField labelWithTitle:@"CPRuleEditor Sentence Localization & Positional Reordering:"]; + var predLabel = [CPTextField labelWithTitle:@"Evaluated Predicate:"]; [predLabel setFrame:CGRectMake(20, 320, 760, 20)]; [predLabel setFont:[CPFont boldSystemFontOfSize:12]]; [contentView addSubview:predLabel]; @@ -67,13 +114,6 @@ [predicateField setFont:[CPFont systemFontOfSize:13]]; [contentView addSubview:predicateField]; - var addBtn = [CPButton buttonWithTitle:@"Añadir regla"]; - [addBtn setFrame:CGRectMake(20, 400, 120, 24)]; - [addBtn setTarget:ruleEditor]; - [addBtn setAction:@selector(addRow:)]; - [contentView addSubview:addBtn]; - - [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(ruleEditorRowsDidChange:) name:CPRuleEditorRowsDidChangeNotification @@ -83,6 +123,26 @@ [self ruleEditorAction:nil]; } +- (void)changeLanguage:(id)sender +{ + var selectedTitle = [sender titleOfSelectedItem], + targetDict = englishDict; + + if ([selectedTitle isEqualToString:@"Spanish"]) + { + targetDict = spanishDict; + } + else if ([selectedTitle isEqualToString:@"German"]) + { + targetDict = germanDict; + } + + [[ruleEditor standardLocalizer] setDictionary:targetDict]; + + // Post notification to trigger localized redraw across editor slices + [[CPNotificationCenter defaultCenter] postNotificationName:@"_CPRuleEditorLocalizerDidLoadNotification" object:[ruleEditor standardLocalizer]]; +} + - (void)ruleEditorAction:(id)sender { var predicate = [ruleEditor predicate]; diff --git a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings b/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings deleted file mode 100644 index 75dd5e735..000000000 --- a/Tests/Manual/CPRuleEditorTestSpanish/Resources/Spanish.strings +++ /dev/null @@ -1,14 +0,0 @@ -/* Spanish formatting patterns for CPRuleEditor */ - -"%[firstName]@ %[is equal to]@ %@" = "%1$[Nombre]@ y %3$@ %2$[son iguales]@"; -"%[firstName]@ %[contains]@ %@" = "%1$[Nombre]@ %2$[contiene]@ %3$@"; - -"%[lastName]@ %[is equal to]@ %@" = "%1$[Apellido]@ y %3$@ %2$[son iguales]@"; -"%[lastName]@ %[contains]@ %@" = "%1$[Apellido]@ %2$[contiene]@ %3$@"; - -"%[age]@ %[is equal to]@ %@" = "%1$[Edad]@ y %3$@ %2$[son iguales]@"; -"%[age]@ is equal to %@" = "%1$[Edad]@ y %3$@ %2$[son iguales]@"; - -"Add row" = "Añadir regla"; -"Delete row" = "Eliminar regla"; -"Add compound row" = "Añadir grupo de reglas"; From e03e0c6fc4821beedd7ba33e07af1fd369e23f42 Mon Sep 17 00:00:00 2001 From: daboe01 Date: Thu, 4 Jun 2026 08:55:32 +0200 Subject: [PATCH 8/8] fixed: exception after deleting the last row --- AppKit/CPRuleEditor/CPRuleEditor.j | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j index 9e5a2cbdd..738b67d9d 100644 --- a/AppKit/CPRuleEditor/CPRuleEditor.j +++ b/AppKit/CPRuleEditor/CPRuleEditor.j @@ -1320,23 +1320,28 @@ TODO: implement while (current_index !== CPNotFound) { - var parentIndex = [self parentRowForRow:current_index], - subrowsIndexes = [self subrowIndexesForRow:parentIndex]; - - if ([subrowsIndexes count] === 1) + var parentIndex = [self parentRowForRow:current_index]; + + // If the row has a valid parent in the editor (i.e. not a root row) + if (parentIndex !== -1) { - if (parentIndex !== -1) - return [CPIndexSet indexSetWithIndex:0]; + var subrowsIndexes = [self subrowIndexesForRow:parentIndex]; - var childlessGranPa = [self _childlessParentsIfSlicesWereDeletedAtIndexes:[CPIndexSet indexSetWithIndex:parentIndex]]; - [childlessParents addIndexes:childlessGranPa]; + // If deleting this row leaves the parent with no remaining child rows + if ([subrowsIndexes count] === 1) + { + [childlessParents addIndex:parentIndex]; + + // Recursively check if deleting this parent row leaves the grandparent childless + var childlessGranPa = [self _childlessParentsIfSlicesWereDeletedAtIndexes:[CPIndexSet indexSetWithIndex:parentIndex]]; + [childlessParents addIndexes:childlessGranPa]; + } } current_index = [indexes indexGreaterThanIndex:current_index]; } return childlessParents; - // (id)-[RuleEditor _includeSubslicesForSlicesAtIndexes:] } - (CPIndexSet)_includeSubslicesForSlicesAtIndexes:(CPIndexSet)indexes