Hello community,
I am struggling to get my C# code correct to convert my user forms into each culture. I have tried for days, but I cannot get my labels to change languages when opening Revit in different language versions. I can get my Ribbon Panel button to change languages using a RibbonResources.resx file in my App.cs, but my FormExport.resx files are not providing language translation values.
public FormExport(Autodesk.Revit.DB.Document doc)
{
CultureInfo cultureName = new CultureInfo(Thread.CurrentThread.CurrentUICulture.Name);
string cultureRef = cultureName.Name;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureRef);
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureRef);
InitializeComponent();
familyDocument = doc;
// Get the localized label text from the resource .resx file
string localizedLabelText = GetLocalizedTextFromResource("lblFamilyName.Text");
string localizedHeaderText = GetLocalizedTextFromResource("lblHeader.Text");
// Set the label text with the localized value
lblFamilyName.Text = localizedLabelText;
lblHeader.Text = localizedHeaderText;
}
I have this function set below my form code :
private string GetLocalizedTextFromResource(string key)
{
try
{
// Load the appropriate resource file based on the user's selected language
ResourceManager resourceManager = new ResourceManager("MyApp.FormExport", typeof(FormExport).Assembly);
CultureInfo currentCulture = Thread.CurrentThread.CurrentUICulture;
// Fetch the localized text for the given key from the resource
string localizedText = resourceManager.GetString(key, currentCulture);
// If the resource for the given key is not found in the selected culture,
// explicitly load the default resource (English) using CultureInfo.InvariantCulture
if (localizedText == null)
{
localizedText = resourceManager.GetString(key, CultureInfo.InvariantCulture);
}
// If the resource is still not found, return the key itself as a fallback
return localizedText ?? key;
}
catch (MissingManifestResourceException ex)
{
// Log the exception
Console.WriteLine($"Resource file not found. Exception: {ex.Message}");
return key; // Return the key itself as a fallback
}
}
My form .resx files are:
FormExport.resx
FormExport.en.resx
FormExport.fr.resx
.....(continued)
Thank you for any assistance you can provide.
Geoff
Solved! Go to Solution.
Solved by GJennings-BM. Go to Solution.
Solved by ricaun. Go to Solution.
You don't say what research you have performed to optimise your setup. Searching around a bit for things like resx and .NET leads me to lots of interesting recommendations like this one:
It includes so much advice that it is impossible to guess what you may or may not already be adhering to...
Good evening and thank you Jeremy for your response.
I am testing options mentioned in the link you sent. Below is just a small list sites I have researched and tested against. It has become very confusing since everyone seems to have a different method. I am simply building a C# .Net app for Revit under the Add-in tab. I have five WinForms. One is a primary WinForms and the other WinForms support secondary operations. This is all been built in Visual Studio 2022 for Revit 2021-2024. The app has been submitted to the app store for publishing. Now, I just need to prepare the WinForms for a larger international base of users.
It should be extremely simple. Again, I have my ribbon panel working for any language, but the translations of my WinForms are my challenge. Beyond the sites listed below, I have spent hours reviewing YouTube videos, ChatGPT, reading manuals, and website blogs. In visual studio, I have done the process of setting each form to 'Localizable' = true, then Language = "France, Spanish,...", customized the forms for .resx files. I have tested with separate manual .resx files, and tons of programming methods. Frustrating for something that is effectively a basic task.
I am hoping someone can provide c# .net guidance showing the code requirements of a single form that references the appropriate .resx based upon the user's Revit language version. I currently have a 'Forms" folder for all of my WinForms and their associated .resx language files.
Create the Multilingual .bundle file
Thank you. Geoff
I remember in another post that someone has a similar problem with Form and multi-language.
The .resx file should swap automatically when depending on the CultureInfo, if the resource is now changed automatically probably your key language is different from the CultureInfo in the application, like en is different from en-US and fr is different from fr-FR.
Just update your resource file name to match the same CultureInfo that Revit uses.
Here is a table with all the Langueges and keys: https://github.com/ricaun/RevitAddin.ResourcesExample
Thank you for your response and excellent quality video of your .resx management. Your methods will be useful on some other apps I plan to create.
I was finally successful at getting my C# Winforms to show in any UICulture languages! I want to share this information with others:
RIBBON PANEL LOCALIZATION:
I added a language switcher file based on Andrey Bushman' sample file. This has been modified to 'RVTLanguages.cs'. The file was placed in my root folder below my C# project name.
In the same location, I also created three .resx files:
RibbonResources.resx (empty and set to 'Internal')
RibbonResources.en-US.resx (all of my panel data - see image below)
RibbonResources.fr-FR.resx (my french translations)
To keep my panel button narrow and allow for wrapping of text, I created two lines in the .resx file
In my App.cs :
public Result OnStartup(UIControlledApplication application)
{
#region READ AND SET THE LANGUAGE ENVIRONMENT USING THE RVTLanguages.cs file
RVTLanguages.Cultures(application.ControlledApplication.Language);
ResourceManager res_mng = new ResourceManager(typeof(RibbonResources));
#endregion
RibbonPanel p = application.CreateRibbonPanel(RibbonResources.ResourceManager.GetString("PanelName"));
string thisAssemblyPath = Assembly.GetExecutingAssembly().Location;
string buttonText1 = RibbonResources.ResourceManager.GetString("CommandName1");
string buttonText2 = RibbonResources.ResourceManager.GetString("CommandName2");
PushButtonData btnMyApp = new PushButtonData("btnMyApp", buttonText1 + "\n" + buttonText2, thisAssemblyPath, typeof(Command).FullName)
{
LargeImage = imgSrc,
Image = imgSrcTB,
LongDescription = RibbonResources.ResourceManager.GetString("CommandDescription"),
ToolTip = RibbonResources.ResourceManager.GetString("CommandToolTip")
};
// set help file to reference .bundle file help.html
string helpFolder = Path.Combine(parentFolder, "help.html");
ContextualHelp contextHelp = new ContextualHelp(
ContextualHelpType.ChmFile, helpFolder);
btnMyApp.SetContextualHelp(contextHelp);
p.AddItem(btnMyApp);
This was all that was required for setting up a localized ribbon button.
CREATING THE LOCALIZED WINFORMS:
I created a folder called "Forms" and then placed all of my WinForms under this folder.
After my forms were created, I then manually created two new .resx files.
for example: "MyForm.en-US.resx" and "MyForm.fr-FR.resx"
These files were placed in the Forms folder along with my main WinForm.
In my Winform code I did the following:
MyForm.cs
public sealed class UICultureSwitcher : IDisposable
{
CultureInfo previous;
public UICultureSwitcher()
{
CultureInfo culture = new CultureInfo(Thread
.CurrentThread.CurrentCulture.Name);
previous = Thread.CurrentThread.CurrentUICulture;
Thread.CurrentThread.CurrentUICulture = culture;
}
void IDisposable.Dispose()
{
Thread.CurrentThread.CurrentUICulture = previous;
}
}
public MyForm(Autodesk.Revit.DB.Document doc)
{
ResourceManager res_mng = new ResourceManager(typeof(MyForm));
ResourceSet resourceSet = res_mng.GetResourceSet(Thread.CurrentThread.CurrentUICulture, true, true);
InitializeComponent();
familyDocument = doc;
label1.Text = GetLocalizedTextFromResource("label1Text");
label2.Text = GetLocalizedTextFromResource("label2.Text");
label3.Text = GetLocalizedTextFromResource("label3.Text");
label4.Text = GetLocalizedTextFromResource("label4.Text");
............
<rest of code>
}
Below I have a function that will collect the necessary language information in the .resx file.
private string GetLocalizedTextFromResource(string key)
{
try
{
// Load the appropriate resource file based on the user's selected language
ResourceManager resourceManager = new ResourceManager("MyApp.Forms.MyForm", typeof(MyForm).Assembly);
CultureInfo currentCulture = Thread.CurrentThread.CurrentUICulture;
// Fetch the localized text for the given key from the resource
string localizedText = resourceManager.GetString(key, currentCulture);
// If the resource for the given key is not found in the culture,
// explicitly load the default resource (English) using CultureInfo.InvariantCulture
if (localizedText == null)
{
localizedText = resourceManager.GetString(key, CultureInfo.InvariantCulture);
}
// If the resource is still not found, return the key itself as a fallback
return localizedText ?? key;
}
catch (MissingManifestResourceException ex)
{
// Handle the exception if the resource file is not found
// Log the exception
Console.WriteLine($"Resource file not found. Exception: {ex.Message}");
return key; // Return the key itself as a fallback
}
}
I then repeat the same methods for my other forms. I have included screenshots of the end results. Now, I am updating the datagrid based upon a similar workflow.
I truly hope this helps others. This has been a very confusing journey. Everything mentioned was done in Visual Studio 2022 and for Revit 2022-2024.
Kind regards,
Geoff Jennings
Neat!
I believe setting the Thread.CurrentThread.CurrentUICulture and Thread.CurrentThread.CurrentCulture is not necessary to make multilanguage works, Revit already starts with the CultureInfo correctly, the only reason to change should be to test if your Form is working in another CurrentUICulture language. Using your class UICultureSwitcher does the trick.
And why are you using CommandName1 and CommandName2 to create the second line, only one CommandName with multiple lines should work, like your CommandDescription that has multiple lines.
You are correct about the CommandName1 and CommandName2. I originally created the two strings before I did the multiline commandDescription. Previously, I had tried the "\r\n" method in the .resx file and it was making it part of a single full string versus giving me two lines of strings.
I change the .resx files for my Ribbon code and generated the Spanish version. The single CommandName with a shift+return allows for the multiple lines in the ribbon button.
As for the language switcher, I have plans for expanding functionality for an upcoming Pro version of the app.
Thanks again for your video and quick response.
Thank you, Geoff, for your travail and sorting out the confusion, and Luiz for your great support!
Edited and summarised for posterity:
https://thebuildingcoder.typepad.com/blog/2023/08/resx-language-management-and-aps-devcons.html
@GJennings-BM Many thanks for the helpful post! I implemented your ribbon methodology in my Add-in and it's working quite well.
Curious if you have any recommendations for handling unsupported languages? For example, the user can set Revit to French (FRA), but at the moment my addin doesn't have a resource file for that language. So RibbonResources.ResourceManager.GetString, just returns null until you switch to a supported language. Is there a good way to make it fail over to an existing resource file?
Hello Matt,
You can set up an try-catch clause in your code. This way, if an .resx file is not found, it will default to a preferred language. In the below example, I setup the failsafe to fr-FR.
=====
public sealed class UICultureSwitcher : IDisposable
{
CultureInfo previous;
public UICultureSwitcher(string cultureName)
{
CultureInfo culture = new CultureInfo(cultureName);
previous = Thread.CurrentThread.CurrentUICulture;
try
{
// Attempt to load the resource file for the specified culture.
ResourceManager rm = new ResourceManager("YourAppNamespace.Resources.Labels", typeof(UICultureSwitcher).Assembly);
rm.GetString("SomeKey", culture); // Use a key from your resource file.
Thread.CurrentThread.CurrentUICulture = culture;
}
catch (MissingManifestResourceException)
{
// If the resource file for the specified culture is not found, fall back to fr-FR.
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr-FR");
}
}
void IDisposable.Dispose()
{
Thread.CurrentThread.CurrentUICulture = previous;
}
}
=====
I hope this helps.
Geoff
Can't find what you're looking for? Ask the community or share your knowledge.