nat_linden avatar nat_linden committed d925475

Introduce new LLDir::findSkinnedFilenames() method. Use as needed.
In a number of different places, for different reasons, the viewer wants to
load a UI-related file that might be overridden by a non-default skin; and
within that skin, might further be overridden by a non-default language.
Apparently, for each of those use cases, every individual developer approached
it as an entirely new problem, solving it idiosyncratically for that one case.
Not only is this a maintenance problem, but it rubs one's nose in the fact
that most such solutions consider only a subset of the relevant skin
directories.
Richard and I evolved an API intended to address all such cases: a central
LLDir method returning a list of relevant pathnames, from most general to most
localized, filtered to present only existing files; plus a couple of
convenience methods to specifically obtain the most general and most localized
available file.
There were several load-skinned-file methods (LLFloater::buildFromFile(),
LLPanel::buildFromFile() and LLUICtrlFactory::createFromFile() -- apparently
cloned-and-modified from each other) that contained funky bolted-on logic to
output the loaded data to an optional passed LLXMLNodePtr param. The trouble
is that passing that param forced each of these methods to subvert its normal
search: specifically for that case, it needed to find the baseline XML file
instead of the localized one. Richard agreed that for the intended usage
(reformatting XML files) we should use XML schema instead, and that the hacky
functionality should be removed. Remove it. Also remove
LLUICtrlFactory::getLocalizedXMLNode(), only used for those three special cases.
Some callers explicitly passed the optional LLXMLNodePtr param as NULL. Remove
that.
Remove LLFloaterUIPreview::displayFloater(save) param, which relied on the
optional output LLXMLNodePtr param. Make onClickSaveFloater() and
onClickSaveAll() emit popupAndPrintWarning() about discontinued functionality.
Recast LLFloater::buildFromFile(), LLPanel::buildFromFile(),
LLUICtrlFactory::createFromFile(), LLNotifications::loadTemplates(),
LLUI::locateSkin(), LLFontRegistry::parseFontInfo(),
LLUIColorTable::loadFromSettings(), LLUICtrlFactory::loadWidgetTemplate(),
LLUICtrlFactory::getLayeredXMLNode(), LLUIImageList::initFromFile(),
LLAppViewer::launchUpdater() and LLMediaCtrl::navigateToLocalPage() to use
findSkinnedFilenames(). (Is LLAppViewer::launchUpdater() ever called any more?
Apparently so -- though the linux-updater.bin logic to process the relevant
command-line switch has been disabled. Shrug.) (Is
LLMediaCtrl::navigateToLocalPage() ever used?? If so, why?)
Remove LLUI::setupPaths(), getXUIPaths(), getSkinPath() and
getLocalizedSkinPath(). Remove the skins/paths.xml file read by setupPaths().
The only configuration it contained was the pair of partial paths "xui/en" and
"xui/[LANGUAGE]" -- hardly likely to change. getSkinPath() specifically
returned the first of these, while getLocalizedSkinPath() specifically
returned the second. This knowledge is now embedded in findSkinnedFilenames().
Also remove paths.xml from viewer_manifest.py.
Remove injected xui_paths from LLFontGL::initClass() and
LLFontRegistry::LLFontRegistry(). These are no longer needed since
LLFontRegistry can now directly consult LLDir for its path search. Stop
passing LLUI::getXUIPaths() to LLFontGL::initClass() in LLViewerWindow's
constructor and initFonts() method.
Add LLDir::append() and add() methods for the simple task of combining two
path components separated by getDirDelimiter() -- but only if they're both
non-empty. Amazing how often that logic is replicated. Replace some existing
concatenations with add() or append().
New LLDir::findSkinnedFilenames() method must know current language. Allow
injecting current language by adding an LLDir::setSkinFolder(language) param,
and pass it where LLAppViewer::init() and initConfiguration() currently call
setSkinFolder(). Also add LLDir::getSkinFolder() and getLanguage() methods.
Change LLFLoaterUIPreview's LLLocalizationResetForcer helper to "forcibly
reset language" using LLDir::setSkinFolder() instead of LLUI::setupPaths().
Update LLDir stubs in lldir_stub.cpp and llupdaterservice_test.cpp.
Add LLDir::getUserDefaultSkinDir() to obtain often-overlooked possible skin
directory -- like getUserSkinDir() but with "default" in place of the current
skin name as the last path component. (However, we hope findSkinnedFilenames()
obviates most explicit use of such individual skin directory pathnames.)
Add LLDir unit tests for new findSkinnedFilenames() and add() methods -- the
latter exercises append() as well.
Tweak indra/integration_tests/llui_libtest/llui_libtest.cpp for all the above.
Notably, comment out its export_test_floaters() function, since the essential
LLFloater::buildFromFile(optional LLXMLNodePtr) functionality has been
removed. This may mean that llui_libtest.cpp has little remaining value, not
sure.

Comments (0)

Files changed (38)

indra/integration_tests/llimage_libtest/llimage_libtest.cpp

 		LLDirIterator iter(dir, name);
 		while (iter.next(next_name))
 		{
-			std::string file_name = dir + gDirUtilp->getDirDelimiter() + next_name;
+			std::string file_name = gDirUtilp->add(dir, next_name);
 			input_filenames.push_back(file_name);
 		}
 	}

indra/integration_tests/llui_libtest/llui_libtest.cpp

 };
 TestImageProvider gTestImageProvider;
 
-static std::string get_xui_dir()
-{
-	std::string delim = gDirUtilp->getDirDelimiter();
-	return gDirUtilp->getSkinBaseDir() + delim + "default" + delim + "xui" + delim;
-}
-
 void init_llui()
 {
 	// Font lookup needs directory support
 	const char* newview_path = "../../../newview";
 #endif
 	gDirUtilp->initAppDirs("SecondLife", newview_path);
-	gDirUtilp->setSkinFolder("default");
+	gDirUtilp->setSkinFolder("default", "en");
 	
 	// colors are no longer stored in a LLControlGroup file
 	LLUIColorTable::instance().loadFromSettings();
 
-	std::string config_filename = gDirUtilp->getExpandedFilename(
-																 LL_PATH_APP_SETTINGS, "settings.xml");
+	std::string config_filename = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "settings.xml");
 	gSavedSettings.loadFromFile(config_filename);
 	
 	// See LLAppViewer::init()
 	
 	const bool no_register_widgets = false;
 	LLWidgetReg::initClass( no_register_widgets );
-	
-	// Unclear if this is needed
-	LLUI::setupPaths();
+
 	// Otherwise we get translation warnings when setting up floaters
 	// (tooltips for buttons)
 	std::set<std::string> default_args;
 	// otherwise it crashes.
 	LLFontGL::initClass(96.f, 1.f, 1.f,
 						gDirUtilp->getAppRODataDir(),
-						LLUI::getXUIPaths(),
 						false );	// don't create gl textures
 	
 	LLFloaterView::Params fvparams;
 	gFloaterView = LLUICtrlFactory::create<LLFloaterView> (fvparams);
 }
 
+/*==========================================================================*|
+static std::string get_xui_dir()
+{
+	std::string delim = gDirUtilp->getDirDelimiter();
+	return gDirUtilp->getSkinBaseDir() + delim + "default" + delim + "xui" + delim;
+}
+
+// buildFromFile() no longer supports generate-output-LLXMLNode
 void export_test_floaters()
 {
 	// Convert all test floaters to new XML format
 		floater->buildFromFile(	filename,
 								//	 FALSE,	// don't open floater
 								output_node);
-		std::string out_filename = xui_dir + filename;
+		std::string out_filename = gDirUtilp->add(xui_dir, filename);
 		std::string::size_type extension_pos = out_filename.rfind(".xml");
 		out_filename.resize(extension_pos);
 		out_filename += "_new.xml";
 		fclose(floater_file);
 	}
 }
+|*==========================================================================*/
 
 int main(int argc, char** argv)
 {
 
 	init_llui();
 	
-	export_test_floaters();
+//	export_test_floaters();
 	
 	return 0;
 }

indra/linux_updater/linux_updater.cpp

 {
 	std::string image_filename;
 	iter.next(image_filename);
-	return image_path + "/" + image_filename;
+	return gDirUtilp->add(image_path, image_filename);
 }
 
 void on_window_closed(GtkWidget *sender, GdkEvent* event, gpointer data)

indra/llrender/llfontgl.cpp

 }
 
 // static
-void LLFontGL::initClass(F32 screen_dpi, F32 x_scale, F32 y_scale, const std::string& app_dir, const std::vector<std::string>& xui_paths, bool create_gl_textures)
+void LLFontGL::initClass(F32 screen_dpi, F32 x_scale, F32 y_scale, const std::string& app_dir, bool create_gl_textures)
 {
 	sVertDPI = (F32)llfloor(screen_dpi * y_scale);
 	sHorizDPI = (F32)llfloor(screen_dpi * x_scale);
 	// Font registry init
 	if (!sFontRegistry)
 	{
-		sFontRegistry = new LLFontRegistry(xui_paths, create_gl_textures);
+		sFontRegistry = new LLFontRegistry(create_gl_textures);
 		sFontRegistry->parseFontInfo("fonts.xml");
 	}
 	else

indra/llrender/llfontgl.h

 	const LLFontDescriptor& getFontDesc() const;
 
 
-	static void initClass(F32 screen_dpi, F32 x_scale, F32 y_scale, const std::string& app_dir, const std::vector<std::string>& xui_paths, bool create_gl_textures = true);
+	static void initClass(F32 screen_dpi, F32 x_scale, F32 y_scale, const std::string& app_dir, bool create_gl_textures = true);
 
 	// Load sans-serif, sans-serif-small, etc.
 	// Slow, requires multiple seconds to load fonts.

indra/llrender/llfontregistry.cpp

 	return LLFontDescriptor(new_name,new_size,new_style,getFileNames());
 }
 
-LLFontRegistry::LLFontRegistry(const string_vec_t& xui_paths,
-							   bool create_gl_textures)
+LLFontRegistry::LLFontRegistry(bool create_gl_textures)
 :	mCreateGLTextures(create_gl_textures)
 {
-	// Propagate this down from LLUICtrlFactory so LLRender doesn't
-	// need an upstream dependency on LLUI.
-	mXUIPaths = xui_paths;
-	
 	// This is potentially a slow directory traversal, so we want to
 	// cache the result.
 	mUltimateFallbackList = LLWindow::getDynamicFallbackFontList();
 
 bool LLFontRegistry::parseFontInfo(const std::string& xml_filename)
 {
-	bool success = false;  // Succeed if we find at least one XUI file
-	const string_vec_t& xml_paths = mXUIPaths;
+	bool success = false;  // Succeed if we find and read at least one XUI file
+	const string_vec_t xml_paths = gDirUtilp->findSkinnedFilenames(LLDir::XUI, xml_filename);
+	if (xml_paths.empty())
+	{
+		// We didn't even find one single XUI file
+		return false;
+	}
+
 	for (string_vec_t::const_iterator path_it = xml_paths.begin();
 		 path_it != xml_paths.end();
 		 ++path_it)
 	{
-	
 		LLXMLNodePtr root;
-		std::string full_filename = gDirUtilp->findSkinnedFilename(*path_it, xml_filename);
-		bool parsed_file = LLXMLNode::parseFile(full_filename, root, NULL);
+		bool parsed_file = LLXMLNode::parseFile(*path_it, root, NULL);
 
 		if (!parsed_file)
 			continue;
-		
+
 		if ( root.isNull() || ! root->hasName( "fonts" ) )
 		{
-			llwarns << "Bad font info file: "
-					<< full_filename << llendl;
+			llwarns << "Bad font info file: " << *path_it << llendl;
 			continue;
 		}
-		
+
 		std::string root_name;
 		root->getAttributeString("name",root_name);
 		if (root->hasName("fonts"))
 	}
 	//if (success)
 	//	dump();
-	
+
 	return success;
 }
 

indra/llrender/llfontregistry.h

 public:
 	// create_gl_textures - set to false for test apps with no OpenGL window,
 	// such as llui_libtest
-	LLFontRegistry(const string_vec_t& xui_paths,
-		bool create_gl_textures);
+	LLFontRegistry(bool create_gl_textures);
 	~LLFontRegistry();
 
 	// Load standard font info from XML file(s).
 	font_size_map_t mFontSizes;
 
 	string_vec_t mUltimateFallbackList;
-	string_vec_t mXUIPaths;
 	bool mCreateGLTextures;
 };
 

indra/llui/llfloater.cpp

 
 static LLFastTimer::DeclareTimer FTM_BUILD_FLOATERS("Build Floaters");
 
-bool LLFloater::buildFromFile(const std::string& filename, LLXMLNodePtr output_node)
+bool LLFloater::buildFromFile(const std::string& filename)
 {
 	LLFastTimer timer(FTM_BUILD_FLOATERS);
 	LLXMLNodePtr root;
 
-	//if exporting, only load the language being exported, 
-	//instead of layering localized version on top of english
-	if (output_node)
+	if (!LLUICtrlFactory::getLayeredXMLNode(filename, root))
 	{
-		if (!LLUICtrlFactory::getLocalizedXMLNode(filename, root))
-		{
-			llwarns << "Couldn't parse floater from: " << LLUI::getLocalizedSkinPath() + gDirUtilp->getDirDelimiter() + filename << llendl;
-			return false;
-		}
-	}
-	else if (!LLUICtrlFactory::getLayeredXMLNode(filename, root))
-	{
-		llwarns << "Couldn't parse floater from: " << LLUI::getSkinPath() + gDirUtilp->getDirDelimiter() + filename << llendl;
+		llwarns << "Couldn't find (or parse) floater from: " << filename << llendl;
 		return false;
 	}
 	
 		getCommitCallbackRegistrar().pushScope();
 		getEnableCallbackRegistrar().pushScope();
 		
-		res = initFloaterXML(root, getParent(), filename, output_node);
+		res = initFloaterXML(root, getParent(), filename, NULL);
 
 		setXMLFilename(filename);
 		

indra/llui/llfloater.h

 
 	// Don't export top/left for rect, only height/width
 	static void setupParamsForExport(Params& p, LLView* parent);
-	bool buildFromFile(const std::string &filename, LLXMLNodePtr output_node = NULL);
+	bool buildFromFile(const std::string &filename);
 
 	boost::signals2::connection setMinimizeCallback( const commit_signal_t::slot_type& cb );
 	boost::signals2::connection setOpenCallback( const commit_signal_t::slot_type& cb );

indra/llui/llfloaterreg.cpp

 					llwarns << "Failed to build floater type: '" << name << "'." << llendl;
 					return NULL;
 				}
-				bool success = res->buildFromFile(xui_file, NULL);
+				bool success = res->buildFromFile(xui_file);
 				if (!success)
 				{
 					llwarns << "Failed to build floater type: '" << name << "'." << llendl;

indra/llui/llnotifications.cpp

 bool LLNotifications::loadTemplates()
 {
 	llinfos << "Reading notifications template" << llendl;
-	std::vector<std::string> search_paths;
-	
-	std::string skin_relative_path = gDirUtilp->getDirDelimiter() + LLUI::getSkinPath() + gDirUtilp->getDirDelimiter() + "notifications.xml";
-	std::string localized_skin_relative_path = gDirUtilp->getDirDelimiter() + LLUI::getLocalizedSkinPath() + gDirUtilp->getDirDelimiter() + "notifications.xml";
-
-	addPathIfExists(gDirUtilp->getDefaultSkinDir() + skin_relative_path, search_paths);
-	addPathIfExists(gDirUtilp->getDefaultSkinDir() + localized_skin_relative_path, search_paths);
-	addPathIfExists(gDirUtilp->getSkinDir() + skin_relative_path, search_paths);
-	addPathIfExists(gDirUtilp->getSkinDir() + localized_skin_relative_path, search_paths);
-	addPathIfExists(gDirUtilp->getUserSkinDir() + skin_relative_path, search_paths);
-	addPathIfExists(gDirUtilp->getUserSkinDir() + localized_skin_relative_path, search_paths);
+	// Passing findSkinnedFilenames(merge=true) makes it output all relevant
+	// pathnames instead of just the ones from the most specific skin.
+	std::vector<std::string> search_paths =
+		gDirUtilp->findSkinnedFilenames(LLDir::XUI, "notifications.xml", true);
 
 	std::string base_filename = search_paths.front();
 	LLXMLNodePtr root;
 	BOOL success  = LLXMLNode::getLayeredXMLNode(root, search_paths);
-	
+
 	if (!success || root.isNull() || !root->hasName( "notifications" ))
 	{
-		llerrs << "Problem reading UI Notifications file: " << base_filename << llendl;
+		llerrs << "Problem reading XML from UI Notifications file: " << base_filename << llendl;
 		return false;
 	}
 
 
 	if(!params.validateBlock())
 	{
-		llerrs << "Problem reading UI Notifications file: " << base_filename << llendl;
+		llerrs << "Problem reading XUI from UI Notifications file: " << base_filename << llendl;
 		return false;
 	}
 
 bool LLNotifications::loadVisibilityRules()
 {
 	const std::string xml_filename = "notification_visibility.xml";
-	std::string full_filename = gDirUtilp->findSkinnedFilename(LLUI::getXUIPaths().front(), xml_filename);
+	// Note that here we're looking for the "en" version, the default
+	// language, rather than the most localized version of this file.
+	std::string full_filename = gDirUtilp->findSkinnedFilenameBaseLang(LLDir::XUI, xml_filename);
 
 	LLNotificationVisibilityRule::Rules params;
 	LLSimpleXUIParser parser;

indra/llui/llpanel.cpp

 //-----------------------------------------------------------------------------
 // buildPanel()
 //-----------------------------------------------------------------------------
-BOOL LLPanel::buildFromFile(const std::string& filename, LLXMLNodePtr output_node, const LLPanel::Params& default_params)
+BOOL LLPanel::buildFromFile(const std::string& filename, const LLPanel::Params& default_params)
 {
 	LLFastTimer timer(FTM_BUILD_PANELS);
 	BOOL didPost = FALSE;
 	LLXMLNodePtr root;
 
-	//if exporting, only load the language being exported, 
-	//instead of layering localized version on top of english
-	if (output_node)
-	{	
-		if (!LLUICtrlFactory::getLocalizedXMLNode(filename, root))
-		{
-			llwarns << "Couldn't parse panel from: " << LLUI::getLocalizedSkinPath() + gDirUtilp->getDirDelimiter() + filename  << llendl;
-			return didPost;
-		}
-	}
-	else if (!LLUICtrlFactory::getLayeredXMLNode(filename, root))
+	if (!LLUICtrlFactory::getLayeredXMLNode(filename, root))
 	{
-		llwarns << "Couldn't parse panel from: " << LLUI::getSkinPath() + gDirUtilp->getDirDelimiter() + filename << llendl;
+		llwarns << "Couldn't parse panel from: " << filename << llendl;
 		return didPost;
 	}
 
 		getCommitCallbackRegistrar().pushScope();
 		getEnableCallbackRegistrar().pushScope();
 		
-		didPost = initPanelXML(root, NULL, output_node, default_params);
+		didPost = initPanelXML(root, NULL, NULL, default_params);
 
 		getCommitCallbackRegistrar().popScope();
 		getEnableCallbackRegistrar().popScope();

indra/llui/llpanel.h

 	LLPanel(const LLPanel::Params& params = getDefaultParams());
 	
 public:
-	BOOL buildFromFile(const std::string &filename, LLXMLNodePtr output_node = NULL, const LLPanel::Params&default_params = getDefaultParams());
+	BOOL buildFromFile(const std::string &filename, const LLPanel::Params& default_params = getDefaultParams());
 
 	static LLPanel* createFactoryPanel(const std::string& name);
 

indra/llui/llui.cpp

 	{}
 };
 
-//static
-void LLUI::setupPaths()
-{
-	std::string filename = gDirUtilp->getExpandedFilename(LL_PATH_SKINS, "paths.xml");
-
-	LLXMLNodePtr root;
-	BOOL success  = LLXMLNode::parseFile(filename, root, NULL);
-	Paths paths;
-
-	if(success)
-	{
-		LLXUIParser parser;
-		parser.readXUI(root, paths, filename);
-	}
-	sXUIPaths.clear();
-	
-	if (success && paths.validateBlock())
-	{
-		LLStringUtil::format_map_t path_args;
-		path_args["[LANGUAGE]"] = LLUI::getLanguage();
-		
-		for (LLInitParam::ParamIterator<Directory>::const_iterator it = paths.directories.begin(), 
-				end_it = paths.directories.end();
-			it != end_it;
-			++it)
-		{
-			std::string path_val_ui;
-			for (LLInitParam::ParamIterator<SubDir>::const_iterator subdir_it = it->subdirs.begin(),
-					subdir_end_it = it->subdirs.end();
-				subdir_it != subdir_end_it;)
-			{
-				path_val_ui += subdir_it->value();
-				if (++subdir_it != subdir_end_it)
-					path_val_ui += gDirUtilp->getDirDelimiter();
-			}
-			LLStringUtil::format(path_val_ui, path_args);
-			if (std::find(sXUIPaths.begin(), sXUIPaths.end(), path_val_ui) == sXUIPaths.end())
-			{
-				sXUIPaths.push_back(path_val_ui);
-			}
-
-		}
-	}
-	else // parsing failed
-	{
-		std::string slash = gDirUtilp->getDirDelimiter();
-		std::string dir = "xui" + slash + "en";
-		llwarns << "XUI::config file unable to open: " << filename << llendl;
-		sXUIPaths.push_back(dir);
-	}
-}
-
 
 //static
 std::string LLUI::locateSkin(const std::string& filename)
 {
-	std::string slash = gDirUtilp->getDirDelimiter();
 	std::string found_file = filename;
-	if (!gDirUtilp->fileExists(found_file))
+	if (gDirUtilp->fileExists(found_file))
 	{
-		found_file = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename); // Should be CUSTOM_SKINS?
+		return found_file;
 	}
-	if (sSettingGroups["config"] && sSettingGroups["config"]->controlExists("Language"))
+
+	found_file = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, filename); // Should be CUSTOM_SKINS?
+	if (gDirUtilp->fileExists(found_file))
 	{
-		if (!gDirUtilp->fileExists(found_file))
-		{
-			std::string localization = getLanguage();
-			std::string local_skin = "xui" + slash + localization + slash + filename;
-			found_file = gDirUtilp->findSkinnedFilename(local_skin);
-		}
+		return found_file;
 	}
-	if (!gDirUtilp->fileExists(found_file))
+
+	found_file = gDirUtilp->findSkinnedFilename(LLDir::XUI, filename);
+	if (! found_file.empty())
 	{
-		std::string local_skin = "xui" + slash + "en" + slash + filename;
-		found_file = gDirUtilp->findSkinnedFilename(local_skin);
+		return found_file;
 	}
-	if (!gDirUtilp->fileExists(found_file))
+
+	found_file = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, filename);
+/*==========================================================================*|
+	// Hmm, if we got this far, previous implementation of this method would
+	// return this last found_file value whether or not it actually exists.
+	if (gDirUtilp->fileExists(found_file))
 	{
-		found_file = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, filename);
+		return found_file;
 	}
+|*==========================================================================*/
 	return found_file;
-}	
+}
 
 //static
 LLVector2 LLUI::getWindowSize()

indra/llui/llui.h

 	// Return the ISO639 language name ("en", "ko", etc.) for the viewer UI.
 	// http://www.loc.gov/standards/iso639-2/php/code_list.php
 	static std::string getLanguage();
-	
-	static void setupPaths();
-	static const std::vector<std::string>& getXUIPaths() { return sXUIPaths; }
-	static std::string getSkinPath() { return sXUIPaths.front(); }
-	static std::string getLocalizedSkinPath() { return sXUIPaths.back(); }  //all files may not exist at the localized path
 
 	//helper functions (should probably move free standing rendering helper functions here)
 	static LLView* getRootView() { return sRootView; }

indra/llui/lluicolortable.cpp

 #include "llui.h"
 #include "lluicolortable.h"
 #include "lluictrlfactory.h"
+#include <boost/foreach.hpp>
 
 LLUIColorTable::ColorParams::ColorParams()
 :	value("value"),
 {
 	bool result = false;
 
-	std::string default_filename = gDirUtilp->getExpandedFilename(LL_PATH_DEFAULT_SKIN, "colors.xml");
-	result |= loadFromFilename(default_filename, mLoadedColors);
-
-	std::string current_filename = gDirUtilp->getExpandedFilename(LL_PATH_TOP_SKIN, "colors.xml");
-	if(current_filename != default_filename)
+	// pass merge=true because we want colors.xml from every skin dir
+	BOOST_FOREACH(std::string colors_path,
+				  gDirUtilp->findSkinnedFilenames(LLDir::SKINBASE, "colors.xml", true))
 	{
-		result |= loadFromFilename(current_filename, mLoadedColors);
-	}
-
-	current_filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SKIN, "colors.xml");
-	if(current_filename != default_filename)
-	{
-		result |= loadFromFilename(current_filename, mLoadedColors);
+		result |= loadFromFilename(colors_path, mLoadedColors);
 	}
 
 	std::string user_filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "colors.xml");

indra/llui/lluictrlfactory.cpp

 
 void LLUICtrlFactory::loadWidgetTemplate(const std::string& widget_tag, LLInitParam::BaseBlock& block)
 {
-	std::string filename = std::string("widgets") + gDirUtilp->getDirDelimiter() + widget_tag + ".xml";
+	std::string filename = gDirUtilp->add("widgets", widget_tag + ".xml");
 	LLXMLNodePtr root_node;
 
-	std::string full_filename = gDirUtilp->findSkinnedFilename(LLUI::getXUIPaths().front(), filename);
+	// Here we're looking for the "en" version, the default-language version
+	// of the file, rather than the localized version.
+	std::string full_filename = gDirUtilp->findSkinnedFilenameBaseLang(LLDir::XUI, filename);
 	if (!full_filename.empty())
 	{
 		LLUICtrlFactory::instance().pushFileName(full_filename);
 bool LLUICtrlFactory::getLayeredXMLNode(const std::string &xui_filename, LLXMLNodePtr& root)
 {
 	LLFastTimer timer(FTM_XML_PARSE);
-	
-	std::vector<std::string> paths;
-	std::string path = gDirUtilp->findSkinnedFilename(LLUI::getSkinPath(), xui_filename);
-	if (!path.empty())
-	{
-		paths.push_back(path);
-	}
-
-	std::string localize_path = gDirUtilp->findSkinnedFilename(LLUI::getLocalizedSkinPath(), xui_filename);
-	if (!localize_path.empty() && localize_path != path)
-	{
-		paths.push_back(localize_path);
-	}
+	std::vector<std::string> paths =
+		gDirUtilp->findSkinnedFilenames(LLDir::XUI, xui_filename);
 
 	if (paths.empty())
 	{
 
 
 //-----------------------------------------------------------------------------
-// getLocalizedXMLNode()
-//-----------------------------------------------------------------------------
-bool LLUICtrlFactory::getLocalizedXMLNode(const std::string &xui_filename, LLXMLNodePtr& root)
-{
-	LLFastTimer timer(FTM_XML_PARSE);
-	std::string full_filename = gDirUtilp->findSkinnedFilename(LLUI::getLocalizedSkinPath(), xui_filename);
-	if (!LLXMLNode::parseFile(full_filename, root, NULL))
-	{
-		return false;
-	}
-	else
-	{
-		return true;
-	}
-}
-
-//-----------------------------------------------------------------------------
 // saveToXML()
 //-----------------------------------------------------------------------------
 S32 LLUICtrlFactory::saveToXML(LLView* viewp, const std::string& filename)
 
 
 void LLUICtrlFactory::pushFileName(const std::string& name) 
-{ 
-	mFileNames.push_back(gDirUtilp->findSkinnedFilename(LLUI::getSkinPath(), name)); 
+{
+	// Here we seem to be looking for the default language file ("en") rather
+	// than the localized one, if any.
+	mFileNames.push_back(gDirUtilp->findSkinnedFilenameBaseLang(LLDir::XUI, name));
 }
 
 void LLUICtrlFactory::popFileName() 
 //static
 std::string LLUICtrlFactory::findSkinnedFilename(const std::string& filename)
 {
-	return gDirUtilp->findSkinnedFilename(LLUI::getSkinPath(), filename);
+	return gDirUtilp->findSkinnedFilenameBaseLang(LLDir::XUI, filename);
 }
 
 //static 

indra/llui/lluictrlfactory.h

 	LLView* createFromXML(LLXMLNodePtr node, LLView* parent, const std::string& filename, const widget_registry_t&, LLXMLNodePtr output_node );
 
 	template<typename T>
-	static T* createFromFile(const std::string &filename, LLView *parent, const widget_registry_t& registry, LLXMLNodePtr output_node = NULL)
+	static T* createFromFile(const std::string &filename, LLView *parent, const widget_registry_t& registry)
 	{
 		T* widget = NULL;
 		
 		{
 			LLXMLNodePtr root_node;
 
-			//if exporting, only load the language being exported, 			
-			//instead of layering localized version on top of english			
-			if (output_node)			
-			{					
-				if (!LLUICtrlFactory::getLocalizedXMLNode(filename, root_node))				
-				{							
-					llwarns << "Couldn't parse XUI file: " <<  filename  << llendl;					
-					goto fail;				
-				}
-			}
-			else if (!LLUICtrlFactory::getLayeredXMLNode(filename, root_node))
+			if (!LLUICtrlFactory::getLayeredXMLNode(filename, root_node))
 			{
 				llwarns << "Couldn't parse XUI file: " << skinned_filename << llendl;
 				goto fail;
 			}
 			
-			LLView* view = getInstance()->createFromXML(root_node, parent, filename, registry, output_node);
+			LLView* view = getInstance()->createFromXML(root_node, parent, filename, registry, NULL);
 			if (view)
 			{
 				widget = dynamic_cast<T*>(view);
 	static void createChildren(LLView* viewp, LLXMLNodePtr node, const widget_registry_t&, LLXMLNodePtr output_node = NULL);
 
 	static bool getLayeredXMLNode(const std::string &filename, LLXMLNodePtr& root);
-	static bool getLocalizedXMLNode(const std::string &xui_filename, LLXMLNodePtr& root);
 
 private:
 	//NOTE: both friend declarations are necessary to keep both gcc and msvc happy

indra/llvfs/lldir.cpp

 #include "lluuid.h"
 
 #include "lldiriterator.h"
+#include "stringize.h"
+#include <boost/foreach.hpp>
+#include <boost/range/begin.hpp>
+#include <boost/range/end.hpp>
+#include <algorithm>
+#include <iomanip>
 
 #if LL_WINDOWS
 #include "lldir_win32.h"
 
 LLDir *gDirUtilp = (LLDir *)&gDirUtil;
 
+/// Values for findSkinnedFilenames(subdir) parameter
+const char
+	*LLDir::XUI      = "xui",
+	*LLDir::TEXTURES = "textures",
+	*LLDir::SKINBASE = "";
+
+static const char* const empty = "";
+
 LLDir::LLDir()
 :	mAppName(""),
 	mExecutablePathAndName(""),
 	mOSCacheDir(""),
 	mCAFile(""),
 	mTempDir(""),
-	mDirDelimiter("/") // fallback to forward slash if not overridden
+	mDirDelimiter("/"), // fallback to forward slash if not overridden
+	mLanguage("en")
 {
 }
 
 	LLDirIterator iter(dirname, mask);
 	while (iter.next(filename))
 	{
-		fullpath = dirname;
-		fullpath += getDirDelimiter();
-		fullpath += filename;
+		fullpath = add(dirname, filename);
 
 		if(LLFile::isdir(fullpath))
 		{
 		}
 		else
 		{
-			res = getOSUserAppDir() + mDirDelimiter + "cache";
+			res = add(getOSUserAppDir(), "cache");
 		}
 	}
 	else
 	{
-		res = getOSCacheDir() + mDirDelimiter + "SecondLife";
+		res = add(getOSCacheDir(), "SecondLife");
 	}
 	return res;
 }
 	return mDirDelimiter;
 }
 
+const std::string& LLDir::getDefaultSkinDir() const
+{
+	return mDefaultSkinDir;
+}
+
 const std::string &LLDir::getSkinDir() const
 {
 	return mSkinDir;
 }
 
+const std::string &LLDir::getUserDefaultSkinDir() const
+{
+    return mUserDefaultSkinDir;
+}
+
 const std::string &LLDir::getUserSkinDir() const
 {
 	return mUserSkinDir;
 }
 
-const std::string& LLDir::getDefaultSkinDir() const
-{
-	return mDefaultSkinDir;
-}
-
 const std::string LLDir::getSkinBaseDir() const
 {
 	return mSkinBaseDir;
 	return mLLPluginDir;
 }
 
+static std::string ELLPathToString(ELLPath location)
+{
+    typedef std::map<ELLPath, const char*> ELLPathMap;
+#define ENT(symbol) ELLPathMap::value_type(symbol, #symbol)
+    static ELLPathMap::value_type init[] =
+    {
+        ENT(LL_PATH_NONE),
+        ENT(LL_PATH_USER_SETTINGS),
+        ENT(LL_PATH_APP_SETTINGS),
+        ENT(LL_PATH_PER_SL_ACCOUNT), // returns/expands to blank string if we don't know the account name yet
+        ENT(LL_PATH_CACHE),
+        ENT(LL_PATH_CHARACTER),
+        ENT(LL_PATH_HELP),
+        ENT(LL_PATH_LOGS),
+        ENT(LL_PATH_TEMP),
+        ENT(LL_PATH_SKINS),
+        ENT(LL_PATH_TOP_SKIN),
+        ENT(LL_PATH_CHAT_LOGS),
+        ENT(LL_PATH_PER_ACCOUNT_CHAT_LOGS),
+        ENT(LL_PATH_USER_SKIN),
+        ENT(LL_PATH_LOCAL_ASSETS),
+        ENT(LL_PATH_EXECUTABLE),
+        ENT(LL_PATH_DEFAULT_SKIN),
+        ENT(LL_PATH_FONTS),
+        ENT(LL_PATH_LAST)
+    };
+#undef ENT
+    static const ELLPathMap sMap(boost::begin(init), boost::end(init));
+
+    ELLPathMap::const_iterator found = sMap.find(location);
+    if (found != sMap.end())
+        return found->second;
+    return STRINGIZE("Invalid ELLPath value " << location);
+}
+
 std::string LLDir::getExpandedFilename(ELLPath location, const std::string& filename) const
 {
 	return getExpandedFilename(location, "", filename);
 		break;
 
 	case LL_PATH_APP_SETTINGS:
-		prefix = getAppRODataDir();
-		prefix += mDirDelimiter;
-		prefix += "app_settings";
+		prefix = add(getAppRODataDir(), "app_settings");
 		break;
 	
 	case LL_PATH_CHARACTER:
-		prefix = getAppRODataDir();
-		prefix += mDirDelimiter;
-		prefix += "character";
+		prefix = add(getAppRODataDir(), "character");
 		break;
 		
 	case LL_PATH_HELP:
 		break;
 		
 	case LL_PATH_USER_SETTINGS:
-		prefix = getOSUserAppDir();
-		prefix += mDirDelimiter;
-		prefix += "user_settings";
+		prefix = add(getOSUserAppDir(), "user_settings");
 		break;
 
 	case LL_PATH_PER_SL_ACCOUNT:
 		prefix = getLindenUserDir();
 		if (prefix.empty())
 		{
-			// if we're asking for the per-SL-account directory but we haven't logged in yet (or otherwise don't know the account name from which to build this string), then intentionally return a blank string to the caller and skip the below warning about a blank prefix.
+			// if we're asking for the per-SL-account directory but we haven't
+			// logged in yet (or otherwise don't know the account name from
+			// which to build this string), then intentionally return a blank
+			// string to the caller and skip the below warning about a blank
+			// prefix.
+			LL_DEBUGS("LLDir") << "getLindenUserDir() not yet set: "
+							   << ELLPathToString(location)
+							   << ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename
+							   << "' => ''" << LL_ENDL;
 			return std::string();
 		}
 		break;
 		break;
 
 	case LL_PATH_LOGS:
-		prefix = getOSUserAppDir();
-		prefix += mDirDelimiter;
-		prefix += "logs";
+		prefix = add(getOSUserAppDir(), "logs");
 		break;
 
 	case LL_PATH_TEMP:
 		break;
 
 	case LL_PATH_LOCAL_ASSETS:
-		prefix = getAppRODataDir();
-		prefix += mDirDelimiter;
-		prefix += "local_assets";
+		prefix = add(getAppRODataDir(), "local_assets");
 		break;
 
 	case LL_PATH_EXECUTABLE:
 		break;
 		
 	case LL_PATH_FONTS:
-		prefix = getAppRODataDir();
-		prefix += mDirDelimiter;
-		prefix += "fonts";
+		prefix = add(getAppRODataDir(), "fonts");
 		break;
 		
 	default:
 		llassert(0);
 	}
 
-	std::string filename = in_filename;
-	if (!subdir2.empty())
+	if (prefix.empty())
 	{
-		filename = subdir2 + mDirDelimiter + filename;
+		llwarns << ELLPathToString(location)
+				<< ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename
+				<< "': prefix is empty, possible bad filename" << llendl;
 	}
 
-	if (!subdir1.empty())
+	std::string expanded_filename = add(add(prefix, subdir1), subdir2);
+	if (expanded_filename.empty() && in_filename.empty())
 	{
-		filename = subdir1 + mDirDelimiter + filename;
+		return "";
 	}
+	// Use explicit concatenation here instead of another add() call. Callers
+	// passing in_filename as "" expect to obtain a pathname ending with
+	// mDirSeparator so they can later directly concatenate with a specific
+	// filename. A caller using add() doesn't care, but there's still code
+	// loose in the system that uses std::string::operator+().
+	expanded_filename += mDirDelimiter;
+	expanded_filename += in_filename;
 
-	if (prefix.empty())
-	{
-		llwarns << "prefix is empty, possible bad filename" << llendl;
-	}
-	
-	std::string expanded_filename;
-	if (!filename.empty())
-	{
-		if (!prefix.empty())
-		{
-			expanded_filename += prefix;
-			expanded_filename += mDirDelimiter;
-			expanded_filename += filename;
-		}
-		else
-		{
-			expanded_filename = filename;
-		}
-	}
-	else if (!prefix.empty())
-	{
-		// Directory only, no file name.
-		expanded_filename = prefix;
-	}
-	else
-	{
-		expanded_filename.assign("");
-	}
-
-	//llinfos << "*** EXPANDED FILENAME: <" << expanded_filename << ">" << llendl;
+	LL_DEBUGS("LLDir") << ELLPathToString(location)
+					   << ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename
+					   << "' => '" << expanded_filename << "'" << LL_ENDL;
 	return expanded_filename;
 }
 
 	return exten;
 }
 
-std::string LLDir::findSkinnedFilename(const std::string &filename) const
+std::string LLDir::findSkinnedFilenameBaseLang(const std::string &subdir,
+											   const std::string &filename,
+											   bool merge) const
 {
-	return findSkinnedFilename("", "", filename);
+	// This implementation is basically just as described in the declaration comments.
+	std::vector<std::string> found(findSkinnedFilenames(subdir, filename, merge));
+	if (found.empty())
+	{
+		return "";
+	}
+	return found.front();
 }
 
-std::string LLDir::findSkinnedFilename(const std::string &subdir, const std::string &filename) const
+std::string LLDir::findSkinnedFilename(const std::string &subdir,
+									   const std::string &filename,
+									   bool merge) const
 {
-	return findSkinnedFilename("", subdir, filename);
+	// This implementation is basically just as described in the declaration comments.
+	std::vector<std::string> found(findSkinnedFilenames(subdir, filename, merge));
+	if (found.empty())
+	{
+		return "";
+	}
+	return found.back();
 }
 
-std::string LLDir::findSkinnedFilename(const std::string &subdir1, const std::string &subdir2, const std::string &filename) const
+std::vector<std::string> LLDir::findSkinnedFilenames(const std::string& subdir,
+													 const std::string& filename,
+													 bool merge) const
 {
-	// generate subdirectory path fragment, e.g. "/foo/bar", "/foo", ""
-	std::string subdirs = ((subdir1.empty() ? "" : mDirDelimiter) + subdir1)
-						 + ((subdir2.empty() ? "" : mDirDelimiter) + subdir2);
+	// Recognize subdirs that have no localization.
+	static const char* sUnlocalizedData[] =
+	{
+		"",							// top-level directory not localized
+		"textures"					// textures not localized
+	};
+	static const std::set<std::string> sUnlocalized(boost::begin(sUnlocalizedData),
+													boost::end(sUnlocalizedData));
 
-	std::vector<std::string> search_paths;
-	
-	search_paths.push_back(getUserSkinDir() + subdirs);		// first look in user skin override
-	search_paths.push_back(getSkinDir() + subdirs);			// then in current skin
-	search_paths.push_back(getDefaultSkinDir() + subdirs);  // then default skin
-	search_paths.push_back(getCacheDir() + subdirs);		// and last in preload directory
+	LL_DEBUGS("LLDir") << "subdir '" << subdir << "', filename '" << filename
+					   << "', merge " << std::boolalpha << merge << LL_ENDL;
 
-	std::string found_file = findFile(filename, search_paths);
-	return found_file;
+	// Cache the default language directory for each subdir we've encountered.
+	// A cache entry whose value is the empty string means "not localized,
+	// don't bother checking again."
+	typedef std::map<std::string, std::string> LocalizedMap;
+	static LocalizedMap sLocalized;
+
+	// Check whether we've already discovered if this subdir is localized.
+	LocalizedMap::const_iterator found = sLocalized.find(subdir);
+	if (found == sLocalized.end())
+	{
+		// We have not yet determined that. Is it one of the subdirs "known"
+		// to be unlocalized?
+		if (sUnlocalized.find(subdir) != sUnlocalized.end())
+		{
+			// This subdir is known to be unlocalized. Remember that.
+			found = sLocalized.insert(LocalizedMap::value_type(subdir, "")).first;
+		}
+		else
+		{
+			// We do not recognize this subdir. Investigate.
+			std::string subdir_path(add(getDefaultSkinDir(), subdir));
+			if (fileExists(add(subdir_path, "en")))
+			{
+				// defaultSkinDir/subdir contains subdir "en". That's our
+				// default language; this subdir is localized. 
+				found = sLocalized.insert(LocalizedMap::value_type(subdir, "en")).first;
+			}
+			else if (fileExists(add(subdir_path, "en-us")))
+			{
+				// defaultSkinDir/subdir contains subdir "en-us" but not "en".
+				// Set as default language; this subdir is localized.
+				found = sLocalized.insert(LocalizedMap::value_type(subdir, "en-us")).first;
+			}
+			else
+			{
+				// defaultSkinDir/subdir contains neither "en" nor "en-us".
+				// Assume it's not localized. Remember that assumption.
+				found = sLocalized.insert(LocalizedMap::value_type(subdir, "")).first;
+			}
+		}
+	}
+	// Every code path above should have resulted in 'found' becoming a valid
+	// iterator to an entry in sLocalized.
+	llassert(found != sLocalized.end());
+
+	// Now -- is this subdir localized, or not? The answer determines what
+	// subdirectories we check (under subdir) for the requested filename.
+	std::vector<std::string> subsubdirs;
+	if (found->second.empty())
+	{
+		// subdir is not localized. filename should be located directly within it.
+		subsubdirs.push_back("");
+	}
+	else
+	{
+		// subdir is localized, and found->second is the default language
+		// directory within it. Check both the default language and the
+		// current language -- if it differs from the default, of course.
+		subsubdirs.push_back(found->second);
+		if (mLanguage != found->second)
+		{
+			subsubdirs.push_back(mLanguage);
+		}
+	}
+	// Code below relies on subsubdirs not being empty: more specifically, on
+	// front() being valid. There may or may not be additional entries, but we
+	// have at least one. For an unlocalized subdir, it's the only one; for a
+	// localized subdir, it's the default one.
+	llassert(! subsubdirs.empty());
+
+	// Build results vector.
+	std::vector<std::string> results;
+	BOOST_FOREACH(std::string skindir, mSearchSkinDirs)
+	{
+		std::string subdir_path(add(skindir, subdir));
+		// Does subdir_path/subsubdirs[0]/filename exist? If there's more than
+		// one entry in subsubdirs, the first is the default language ("en"),
+		// the second is the current language. A skin that contains
+		// subdir/language/filename without also containing subdir/en/filename
+		// is ill-formed: skip any such skin. So to decide whether to keep
+		// this skin dir or skip it, we need only check for the existence of
+		// the first subsubdir entry ("en" or only).
+		std::string subsubdir_path(add(add(subdir_path, subsubdirs.front()), filename));
+		if (! fileExists(subsubdir_path))
+			continue;
+
+		// Here the desired filename exists in the first subsubdir. That means
+		// this is a skindir we want to record in results. But if the caller
+		// passed merge=false, we must discard all previous skindirs.
+		if (! merge)
+		{
+			results.clear();
+		}
+
+		// Now add every subsubdir in which filename exists. We already know
+		// it exists in the first one.
+		results.push_back(subsubdir_path);
+
+		// Append all remaining subsubdirs in which filename exists.
+		for (std::vector<std::string>::const_iterator ssdi(subsubdirs.begin() + 1), ssdend(subsubdirs.end());
+			 ssdi != ssdend; ++ssdi)
+		{
+			subsubdir_path = add(add(subdir_path, *ssdi), filename);
+			if (fileExists(subsubdir_path))
+			{
+				results.push_back(subsubdir_path);
+			}
+		}
+	}
+
+	LL_DEBUGS("LLDir") << empty;
+	const char* comma = "";
+	BOOST_FOREACH(std::string path, results)
+	{
+		LL_CONT << comma << "'" << path << "'";
+		comma = ", ";
+	}
+	LL_CONT << LL_ENDL;
+
+	return results;
 }
 
 std::string LLDir::getTempFilename() const
 	random_uuid.generate();
 	random_uuid.toString(uuid_str);
 
-	std::string temp_filename = getTempDir();
-	temp_filename += mDirDelimiter;
-	temp_filename += uuid_str;
-	temp_filename += ".tmp";
-
-	return temp_filename;
+	return add(getTempDir(), uuid_str + ".tmp");
 }
 
 // static
 		std::string userlower(username);
 		LLStringUtil::toLower(userlower);
 		LLStringUtil::replaceChar(userlower, ' ', '_');
-		mLindenUserDir = getOSUserAppDir();
-		mLindenUserDir += mDirDelimiter;
-		mLindenUserDir += userlower;
+		mLindenUserDir = add(getOSUserAppDir(), userlower);
 	}
 	else
 	{
 		std::string userlower(username);
 		LLStringUtil::toLower(userlower);
 		LLStringUtil::replaceChar(userlower, ' ', '_');
-		mPerAccountChatLogsDir = getChatLogsDir();
-		mPerAccountChatLogsDir += mDirDelimiter;
-		mPerAccountChatLogsDir += userlower;
+		mPerAccountChatLogsDir = add(getChatLogsDir(), userlower);
 	}
 	else
 	{
 	
 }
 
-void LLDir::setSkinFolder(const std::string &skin_folder)
+void LLDir::setSkinFolder(const std::string &skin_folder, const std::string& language)
 {
-	mSkinDir = getSkinBaseDir();
-	mSkinDir += mDirDelimiter;
-	mSkinDir += skin_folder;
+	LL_DEBUGS("LLDir") << "Setting skin '" << skin_folder << "', language '" << language << "'"
+					   << LL_ENDL;
+	mSkinName = skin_folder;
+	mLanguage = language;
 
-	// user modifications to current skin
-	// e.g. c:\documents and settings\users\username\application data\second life\skins\dazzle
-	mUserSkinDir = getOSUserAppDir();
-	mUserSkinDir += mDirDelimiter;
-	mUserSkinDir += "skins";
-	mUserSkinDir += mDirDelimiter;	
-	mUserSkinDir += skin_folder;
+	// This method is called multiple times during viewer initialization. Each
+	// time it's called, reset mSearchSkinDirs.
+	mSearchSkinDirs.clear();
 
 	// base skin which is used as fallback for all skinned files
 	// e.g. c:\program files\secondlife\skins\default
 	mDefaultSkinDir = getSkinBaseDir();
-	mDefaultSkinDir += mDirDelimiter;	
-	mDefaultSkinDir += "default";
+	append(mDefaultSkinDir, "default");
+	// This is always the most general of the search skin directories.
+	addSearchSkinDir(mDefaultSkinDir);
+
+	mSkinDir = getSkinBaseDir();
+	append(mSkinDir, skin_folder);
+	// Next level of generality is a skin installed with the viewer.
+	addSearchSkinDir(mSkinDir);
+
+	// user modifications to skins, current and default
+	// e.g. c:\documents and settings\users\username\application data\second life\skins\dazzle
+	mUserSkinDir = getOSUserAppDir();
+	append(mUserSkinDir, "skins");
+	mUserDefaultSkinDir = mUserSkinDir;
+	append(mUserDefaultSkinDir, "default");
+	append(mUserSkinDir, skin_folder);
+	// Next level of generality is user modifications to default skin...
+	addSearchSkinDir(mUserDefaultSkinDir);
+	// then user-defined skins.
+	addSearchSkinDir(mUserSkinDir);
+}
+
+void LLDir::addSearchSkinDir(const std::string& skindir)
+{
+	if (std::find(mSearchSkinDirs.begin(), mSearchSkinDirs.end(), skindir) == mSearchSkinDirs.end())
+	{
+		LL_DEBUGS("LLDir") << "search skin: '" << skindir << "'" << LL_ENDL;
+		mSearchSkinDirs.push_back(skindir);
+	}
+}
+
+std::string LLDir::getSkinFolder() const
+{
+	return mSkinName;
+}
+
+std::string LLDir::getLanguage() const
+{
+	return mLanguage;
 }
 
 bool LLDir::setCacheDir(const std::string &path)
 	else
 	{
 		LLFile::mkdir(path);
-		std::string tempname = path + mDirDelimiter + "temp";
+		std::string tempname = add(path, "temp");
 		LLFILE* file = LLFile::fopen(tempname,"wt");
 		if (file)
 		{
 	LL_DEBUGS2("AppInit","Directories") << "  SkinDir:               " << getSkinDir() << LL_ENDL;
 }
 
+std::string LLDir::add(const std::string& path, const std::string& name) const
+{
+	std::string destpath(path);
+	append(destpath, name);
+	return destpath;
+}
+
+void LLDir::append(std::string& destpath, const std::string& name) const
+{
+	// Delegate question of whether we need a separator to helper method.
+	SepOff sepoff(needSep(destpath, name));
+	if (sepoff.first)               // do we need a separator?
+	{
+		destpath += mDirDelimiter;
+	}
+	// If destpath ends with a separator, AND name starts with one, skip
+	// name's leading separator.
+	destpath += name.substr(sepoff.second);
+}
+
+LLDir::SepOff LLDir::needSep(const std::string& path, const std::string& name) const
+{
+	if (path.empty() || name.empty())
+	{
+		// If either path or name are empty, we do not need a separator
+		// between them.
+		return SepOff(false, 0);
+	}
+	// Here we know path and name are both non-empty. But if path already ends
+	// with a separator, or if name already starts with a separator, we need
+	// not add one.
+	std::string::size_type seplen(mDirDelimiter.length());
+	bool path_ends_sep(path.substr(path.length() - seplen) == mDirDelimiter);
+	bool name_starts_sep(name.substr(0, seplen) == mDirDelimiter);
+	if ((! path_ends_sep) && (! name_starts_sep))
+	{
+		// If neither path nor name brings a separator to the junction, then
+		// we need one.
+		return SepOff(true, 0);
+	}
+	if (path_ends_sep && name_starts_sep)
+	{
+		// But if BOTH path and name bring a separator, we need not add one.
+		// Moreover, we should actually skip the leading separator of 'name'.
+		return SepOff(false, seplen);
+	}
+	// Here we know that either path_ends_sep or name_starts_sep is true --
+	// but not both. So don't add a separator, and don't skip any characters:
+	// simple concatenation will do the trick.
+	return SepOff(false, 0);
+}
 
 void dir_exists_or_crash(const std::string &dir_name)
 {

indra/llvfs/lldir.h

 	LL_PATH_LAST
 } ELLPath;
 
-
+/// Directory operations
 class LLDir
 {
  public:
 	const std::string &getOSCacheDir() const;		// location of OS-specific cache folder (may be empty string)
 	const std::string &getCAFile() const;			// File containing TLS certificate authorities
 	const std::string &getDirDelimiter() const;	// directory separator for platform (ie. '\' or '/' or ':')
+	const std::string &getDefaultSkinDir() const;	// folder for default skin. e.g. c:\program files\second life\skins\default
 	const std::string &getSkinDir() const;		// User-specified skin folder.
+	const std::string &getUserDefaultSkinDir() const; // dir with user modifications to default skin
 	const std::string &getUserSkinDir() const;		// User-specified skin folder with user modifications. e.g. c:\documents and settings\username\application data\second life\skins\curskin
-	const std::string &getDefaultSkinDir() const;	// folder for default skin. e.g. c:\program files\second life\skins\default
 	const std::string getSkinBaseDir() const;		// folder that contains all installed skins (not user modifications). e.g. c:\program files\second life\skins
 	const std::string &getLLPluginDir() const;		// Directory containing plugins and plugin shell
 
 	std::string getExtension(const std::string& filepath) const; // Excludes '.', e.g getExtension("foo.wav") == "wav"
 
 	// these methods search the various skin paths for the specified file in the following order:
-	// getUserSkinDir(), getSkinDir(), getDefaultSkinDir()
-	std::string findSkinnedFilename(const std::string &filename) const;
-	std::string findSkinnedFilename(const std::string &subdir, const std::string &filename) const;
-	std::string findSkinnedFilename(const std::string &subdir1, const std::string &subdir2, const std::string &filename) const;
+	// getUserSkinDir(), getUserDefaultSkinDir(), getSkinDir(), getDefaultSkinDir()
+	/**
+	 * Given a filename within skin, return an ordered sequence of paths to
+	 * search. Nonexistent files will be filtered out -- which means that the
+	 * vector might be empty.
+	 *
+	 * @param subdir Identify top-level skin subdirectory by passing one of
+	 * LLDir::XUI (file lives under "xui" subtree), LLDir::TEXTURES (file
+	 * lives under "textures" subtree), LLDir::SKINBASE (file lives at top
+	 * level of skin subdirectory).
+	 * @param filename Desired filename within subdir within skin, e.g.
+	 * "panel_login.xml". DO NOT prepend (e.g.) "xui" or the desired language.
+	 * @param merge Callers perform two different kinds of processing. When
+	 * fetching a XUI file, for instance, the existence of @a filename in the
+	 * specified skin completely supercedes any @a filename in the default
+	 * skin. For that case, leave the default @a merge=false. The returned
+	 * vector will contain only
+	 * ".../<i>current_skin</i>/xui/en/<i>filename</i>",
+	 * ".../<i>current_skin</i>/xui/<i>current_language</i>/<i>filename</i>".
+	 * But for (e.g.) "strings.xml", we want a given skin to be able to
+	 * override only specific entries from the default skin. Any string not
+	 * defined in the specified skin will be sought in the default skin. For
+	 * that case, pass @a merge=true. The returned vector will contain at
+	 * least ".../default/xui/en/strings.xml",
+	 * ".../default/xui/<i>current_language</i>/strings.xml",
+	 * ".../<i>current_skin</i>/xui/en/strings.xml",
+	 * ".../<i>current_skin</i>/xui/<i>current_language</i>/strings.xml".
+	 */
+	std::vector<std::string> findSkinnedFilenames(const std::string& subdir,
+												  const std::string& filename,
+												  bool merge=false) const;
+	/// Values for findSkinnedFilenames(subdir) parameter
+	static const char *XUI, *TEXTURES, *SKINBASE;
+	/**
+	 * Return the base-language pathname from findSkinnedFilenames(), or
+	 * the empty string if no such file exists. Parameters are identical to
+	 * findSkinnedFilenames(). This is shorthand for capturing the vector
+	 * returned by findSkinnedFilenames(), checking for empty() and then
+	 * returning front().
+	 */
+	std::string findSkinnedFilenameBaseLang(const std::string &subdir,
+											const std::string &filename,
+											bool merge=false) const;
+	/**
+	 * Return the "most localized" pathname from findSkinnedFilenames(), or
+	 * the empty string if no such file exists. Parameters are identical to
+	 * findSkinnedFilenames(). This is shorthand for capturing the vector
+	 * returned by findSkinnedFilenames(), checking for empty() and then
+	 * returning back().
+	 */
+	std::string findSkinnedFilename(const std::string &subdir,
+									const std::string &filename,
+									bool merge=false) const;
 
 	// random filename in common temporary directory
 	std::string getTempFilename() const;
 	virtual void setChatLogsDir(const std::string &path);		// Set the chat logs dir to this user's dir
 	virtual void setPerAccountChatLogsDir(const std::string &username);		// Set the per user chat log directory.
 	virtual void setLindenUserDir(const std::string &username);		// Set the linden user dir to this user's dir
-	virtual void setSkinFolder(const std::string &skin_folder);
+	virtual void setSkinFolder(const std::string &skin_folder, const std::string& language);
+	virtual std::string getSkinFolder() const;
+	virtual std::string getLanguage() const;
 	virtual bool setCacheDir(const std::string &path);
 
 	virtual void dumpCurrentDirectories();
-	
+
 	// Utility routine
 	std::string buildSLOSCacheDir() const;
 
+	/// Append specified @a name to @a destpath, separated by getDirDelimiter()
+	/// if both are non-empty.
+	void append(std::string& destpath, const std::string& name) const;
+	/// Append specified @a name to @a path, separated by getDirDelimiter()
+	/// if both are non-empty. Return result, leaving @a path unmodified.
+	std::string add(const std::string& path, const std::string& name) const;
+
 protected:
+	// Does an add() or append() call need a directory delimiter?
+	typedef std::pair<bool, unsigned short> SepOff;
+	SepOff needSep(const std::string& path, const std::string& name) const;
+	// build mSearchSkinDirs without adding duplicates
+	void addSearchSkinDir(const std::string& skindir);
+
 	std::string mAppName;               // install directory under progams/ ie "SecondLife"   
 	std::string mExecutablePathAndName; // full path + Filename of .exe
 	std::string mExecutableFilename;    // Filename of .exe
 	std::string mDefaultCacheDir;	// default cache diretory
 	std::string mOSCacheDir;		// operating system cache dir
 	std::string mDirDelimiter;
+	std::string mSkinName;           // caller-specified skin name
 	std::string mSkinBaseDir;			// Base for skins paths.
+	std::string mDefaultSkinDir;			// Location for default skin info.
 	std::string mSkinDir;			// Location for current skin info.
-	std::string mDefaultSkinDir;			// Location for default skin info.
+	std::string mUserDefaultSkinDir;		// Location for default skin info.
 	std::string mUserSkinDir;			// Location for user-modified skin info.
+	// Skin directories to search, most general to most specific. This order
+	// works well for composing fine-grained files, in which an individual item
+	// in a specific file overrides the corresponding item in more general
+	// files. Of course, for a file-level search, iterate backwards.
+	std::vector<std::string> mSearchSkinDirs;
+	std::string mLanguage;              // Current viewer language
 	std::string mLLPluginDir;			// Location for plugins and plugin shell
 };
 

indra/llvfs/tests/lldir_test.cpp

 
 #include "linden_common.h"
 
+#include "llstring.h"
+#include "tests/StringVec.h"
 #include "../lldir.h"
 #include "../lldiriterator.h"
 
 #include "../test/lltut.h"
+#include "stringize.h"
+#include <boost/foreach.hpp>
+#include <boost/assign/list_of.hpp>
 
+using boost::assign::list_of;
+
+// We use ensure_equals(..., vec(list_of(...))) not because it's functionally
+// required, but because ensure_equals() knows how to format a StringVec.
+// Turns out that when ensure_equals() displays a test failure with just
+// list_of("string")("another"), you see 'stringanother' vs. '("string",
+// "another")'.
+StringVec vec(const StringVec& v)
+{
+    return v;
+}
+
+// For some tests, use a dummy LLDir that uses memory data instead of touching
+// the filesystem
+struct LLDir_Dummy: public LLDir
+{
+    /*----------------------------- LLDir API ------------------------------*/
+    LLDir_Dummy()
+    {
+        // Initialize important LLDir data members based on the filesystem
+        // data below.
+        mDirDelimiter = "/";
+        mExecutableDir = "install";
+        mExecutableFilename = "test";
+        mExecutablePathAndName = add(mExecutableDir, mExecutableFilename);
+        mWorkingDir = mExecutableDir;
+        mAppRODataDir = "install";
+        mSkinBaseDir = add(mAppRODataDir, "skins");
+        mOSUserDir = "user";
+        mOSUserAppDir = mOSUserDir;
+        mLindenUserDir = "";
+
+        // Make the dummy filesystem look more or less like what we expect in
+        // the real one.
+        static const char* preload[] =
+        {
+            "install/skins/default/colors.xml",
+            "install/skins/default/xui/en/strings.xml",
+            "install/skins/default/xui/fr/strings.xml",
+            "install/skins/default/xui/en/floater.xml",
+            "install/skins/default/xui/fr/floater.xml",
+            "install/skins/default/xui/en/newfile.xml",
+            "install/skins/default/xui/fr/newfile.xml",
+            "install/skins/default/html/en-us/welcome.html",
+            "install/skins/default/html/fr/welcome.html",
+            "install/skins/default/textures/only_default.jpeg",
+            "install/skins/default/future/somefile.txt",
+            "install/skins/steam/colors.xml",
+            "install/skins/steam/xui/en/strings.xml",
+            "install/skins/steam/xui/fr/strings.xml",
+            "install/skins/steam/textures/only_steam.jpeg",
+            "user/skins/default/colors.xml",
+            "user/skins/default/xui/en/strings.xml",
+            "user/skins/default/xui/fr/strings.xml",
+            // This is an attempted override that doesn't work: for a
+            // localized subdir, a skin must have subdir/en/filename as well
+            // as subdir/language/filename.
+            "user/skins/default/xui/fr/floater.xml",
+            // This is an override that only specifies the "en" version
+            "user/skins/default/xui/en/newfile.xml",
+            "user/skins/default/textures/only_user_default.jpeg",
+            "user/skins/steam/colors.xml",
+            "user/skins/steam/xui/en/strings.xml",
+            "user/skins/steam/xui/fr/strings.xml",
+            "user/skins/steam/textures/only_user_steam.jpeg"
+        };
+        BOOST_FOREACH(const char* path, preload)
+        {
+            buildFilesystem(path);
+        }
+    }
+
+    virtual ~LLDir_Dummy() {}
+
+    virtual void initAppDirs(const std::string& app_name, const std::string& app_read_only_data_dir)
+    {
+        // Implement this when we write a test that needs it
+    }
+
+    virtual std::string getCurPath()
+    {
+        // Implement this when we write a test that needs it
+        return "";
+    }
+
+    virtual U32 countFilesInDir(const std::string& dirname, const std::string& mask)
+    {
+        // Implement this when we write a test that needs it
+        return 0;
+    }
+
+    virtual BOOL fileExists(const std::string& pathname) const
+    {
+        // Record fileExists() calls so we can check whether caching is
+        // working right. Certain LLDir calls should be able to make decisions
+        // without calling fileExists() again, having already checked existence.
+        mChecked.insert(pathname);
+        // For our simple flat set of strings, see whether the identical
+        // pathname exists in our set.
+        return (mFilesystem.find(pathname) != mFilesystem.end());
+    }
+
+    virtual std::string getLLPluginLauncher()
+    {
+        // Implement this when we write a test that needs it
+        return "";
+    }
+
+    virtual std::string getLLPluginFilename(std::string base_name)
+    {
+        // Implement this when we write a test that needs it
+        return "";
+    }
+
+    /*----------------------------- Dummy data -----------------------------*/
+    void clearFilesystem() { mFilesystem.clear(); }
+    void buildFilesystem(const std::string& path)
+    {
+        // Split the pathname on slashes, ignoring leading, trailing, doubles
+        StringVec components;
+        LLStringUtil::getTokens(path, components, "/");
+        // Ensure we have an entry representing every level of this path
+        std::string partial;
+        BOOST_FOREACH(std::string component, components)
+        {
+            append(partial, component);
+            mFilesystem.insert(partial);
+        }
+    }
+
+    void clear_checked() { mChecked.clear(); }
+    void ensure_checked(const std::string& pathname) const
+    {
+        tut::ensure(STRINGIZE(pathname << " was not checked but should have been"),
+                    mChecked.find(pathname) != mChecked.end());
+    }
+    void ensure_not_checked(const std::string& pathname) const
+    {
+        tut::ensure(STRINGIZE(pathname << " was checked but should not have been"),
+                    mChecked.find(pathname) == mChecked.end());
+    }
+
+    std::set<std::string> mFilesystem;
+    mutable std::set<std::string> mChecked;
+};
 
 namespace tut
 {
       LLFile::rmdir(dir1);
       LLFile::rmdir(dir2);
    }
+
+    template<> template<>
+    void LLDirTest_object_t::test<6>()
+    {
+        set_test_name("findSkinnedFilenames()");
+        LLDir_Dummy lldir;
+        /*------------------------ "default", "en" -------------------------*/
+        // Setting "default" means we shouldn't consider any "*/skins/steam"
+        // directories; setting "en" means we shouldn't consider any "xui/fr"
+        // directories.
+        lldir.setSkinFolder("default", "en");
+        ensure_equals(lldir.getSkinFolder(), "default");
+        ensure_equals(lldir.getLanguage(), "en");
+
+        // top-level directory of a skin isn't localized
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::SKINBASE, "colors.xml", true),
+                      vec(list_of("install/skins/default/colors.xml")
+                                 ("user/skins/default/colors.xml")));
+        // We should not have needed to check for skins/default/en. We should
+        // just "know" that SKINBASE is not localized.
+        lldir.ensure_not_checked("install/skins/default/en");
+
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_default.jpeg"),
+                      vec(list_of("install/skins/default/textures/only_default.jpeg")));
+        // Nor should we have needed to check skins/default/textures/en
+        // because textures is known to be unlocalized.
+        lldir.ensure_not_checked("install/skins/default/textures/en");
+
+        StringVec expected(vec(list_of("install/skins/default/xui/en/strings.xml")
+                               ("user/skins/default/xui/en/strings.xml")));
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true),
+                      expected);
+        // The first time, we had to probe to find out whether xui was localized.
+        lldir.ensure_checked("install/skins/default/xui/en");
+        lldir.clear_checked();
+        // Now make the same call again -- should return same result --
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true),
+                      expected);
+        // but this time it should remember that xui is localized.
+        lldir.ensure_not_checked("install/skins/default/xui/en");
+
+        // localized subdir with "en-us" instead of "en"
+        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"),
+                      vec(list_of("install/skins/default/html/en-us/welcome.html")));
+        lldir.ensure_checked("install/skins/default/html/en");
+        lldir.ensure_checked("install/skins/default/html/en-us");
+        lldir.clear_checked();
+        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"),
+                      vec(list_of("install/skins/default/html/en-us/welcome.html")));
+        lldir.ensure_not_checked("install/skins/default/html/en");
+        lldir.ensure_not_checked("install/skins/default/html/en-us");
+
+        ensure_equals(lldir.findSkinnedFilenames("future", "somefile.txt"),
+                      vec(list_of("install/skins/default/future/somefile.txt")));
+        // Test probing for an unrecognized unlocalized future subdir.
+        lldir.ensure_checked("install/skins/default/future/en");
+        lldir.clear_checked();
+        ensure_equals(lldir.findSkinnedFilenames("future", "somefile.txt"),
+                      vec(list_of("install/skins/default/future/somefile.txt")));
+        // Second time it should remember that future is unlocalized.
+        lldir.ensure_not_checked("install/skins/default/future/en");
+
+        // When language is set to "en", requesting an html file pulls up the
+        // "en-us" version -- not because it magically matches those strings,
+        // but because there's no "en" localization and it falls back on the
+        // default "en-us"! Note that it would probably still be better to
+        // make the default localization be "en" and allow "en-gb" (or
+        // whatever) localizations, which would work much more the way you'd
+        // expect.
+        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"),
+                      vec(list_of("install/skins/default/html/en-us/welcome.html")));
+
+        /*------------------------ "default", "fr" -------------------------*/
+        // We start being able to distinguish localized subdirs from
+        // unlocalized when we ask for a non-English language.
+        lldir.setSkinFolder("default", "fr");
+        ensure_equals(lldir.getLanguage(), "fr");
+
+        // pass merge=true to request this filename in all relevant skins
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true),
+                      vec(list_of
+                          ("install/skins/default/xui/en/strings.xml")
+                          ("install/skins/default/xui/fr/strings.xml")
+                          ("user/skins/default/xui/en/strings.xml")
+                          ("user/skins/default/xui/fr/strings.xml")));
+
+        // pass (or default) merge=false to request only most specific skin
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"),
+                      vec(list_of
+                          ("user/skins/default/xui/en/strings.xml")
+                          ("user/skins/default/xui/fr/strings.xml")));
+
+        // The most specific skin for our dummy floater.xml is the installed
+        // default. Although we have a user xui/fr/floater.xml, we would also
+        // need a xui/en/floater.xml file to consider the user skin for this.
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "floater.xml"),
+                      vec(list_of
+                          ("install/skins/default/xui/en/floater.xml")
+                          ("install/skins/default/xui/fr/floater.xml")));
+
+        // The user override for the default skin does define newfile.xml, but
+        // only an "en" version, not a "fr" version as well. Nonetheless
+        // that's the most specific skin we have, regardless of the existence
+        // of a "fr" version in the installed default skin.
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "newfile.xml"),
+                      vec(list_of("user/skins/default/xui/en/newfile.xml")));
+
+        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"),
+                      vec(list_of
+                          ("install/skins/default/html/en-us/welcome.html")
+                          ("install/skins/default/html/fr/welcome.html")));
+
+        /*------------------------ "default", "zh" -------------------------*/
+        lldir.setSkinFolder("default", "zh");
+        // Because the user default skins strings.xml has only a "fr" override
+        // but not a "zh" override, the most localized version we can find is "en".
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"),
+                      vec(list_of("user/skins/default/xui/en/strings.xml")));
+
+        /*------------------------- "steam", "en" --------------------------*/
+        lldir.setSkinFolder("steam", "en");
+
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::SKINBASE, "colors.xml", true),
+                      vec(list_of
+                          ("install/skins/default/colors.xml")
+                          ("install/skins/steam/colors.xml")
+                          ("user/skins/default/colors.xml")
+                          ("user/skins/steam/colors.xml")));
+
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_default.jpeg"),
+                      vec(list_of("install/skins/default/textures/only_default.jpeg")));
+
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_steam.jpeg"),
+                      vec(list_of("install/skins/steam/textures/only_steam.jpeg")));
+
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_user_default.jpeg"),
+                      vec(list_of("user/skins/default/textures/only_user_default.jpeg")));
+
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_user_steam.jpeg"),
+                      vec(list_of("user/skins/steam/textures/only_user_steam.jpeg")));
+
+        // merge=false
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"),
+                      vec(list_of("user/skins/steam/xui/en/strings.xml")));
+
+        // pass merge=true to request this filename in all relevant skins
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true),
+                      vec(list_of
+                          ("install/skins/default/xui/en/strings.xml")
+                          ("install/skins/steam/xui/en/strings.xml")
+                          ("user/skins/default/xui/en/strings.xml")
+                          ("user/skins/steam/xui/en/strings.xml")));
+
+        /*------------------------- "steam", "fr" --------------------------*/
+        lldir.setSkinFolder("steam", "fr");
+
+        // pass merge=true to request this filename in all relevant skins
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"),
+                      vec(list_of
+                          ("user/skins/steam/xui/en/strings.xml")
+                          ("user/skins/steam/xui/fr/strings.xml")));
+
+        // pass merge=true to request this filename in all relevant skins
+        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true),
+                      vec(list_of
+                          ("install/skins/default/xui/en/strings.xml")
+                          ("install/skins/default/xui/fr/strings.xml")
+                          ("install/skins/steam/xui/en/strings.xml")
+                          ("install/skins/steam/xui/fr/strings.xml")
+                          ("user/skins/default/xui/en/strings.xml")
+                          ("user/skins/default/xui/fr/strings.xml")
+                          ("user/skins/steam/xui/en/strings.xml")
+                          ("user/skins/steam/xui/fr/strings.xml")));
+    }
+
+    template<> template<>
+    void LLDirTest_object_t::test<7>()
+    {
+        set_test_name("add()");
+        LLDir_Dummy lldir;
+        ensure_equals("both empty", lldir.add("", ""), "");
+        ensure_equals("path empty", lldir.add("", "b"), "b");
+        ensure_equals("name empty", lldir.add("a", ""), "a");
+        ensure_equals("both simple", lldir.add("a", "b"), "a/b");
+        ensure_equals("name leading slash", lldir.add("a", "/b"), "a/b");
+        ensure_equals("path trailing slash", lldir.add("a/", "b"), "a/b");
+        ensure_equals("both bring slashes", lldir.add("a/", "/b"), "a/b");
+    }
 }
-

indra/newview/llappviewer.cpp

 #include <boost/algorithm/string.hpp>
 
 
-
 #if LL_WINDOWS
 #	include <share.h> // For _SH_DENYWR in initMarkerFile
 #else
 	gDirUtilp->initAppDirs("SecondLife");
 	// set skin search path to default, will be overridden later
 	// this allows simple skinned file lookups to work
-	gDirUtilp->setSkinFolder("default");
+	gDirUtilp->setSkinFolder("default", "en");
 
 	initLogging();
 	
 		&LLUI::sGLScaleFactor);
 	LL_INFOS("InitInfo") << "UI initialized." << LL_ENDL ;
 
-	// Setup paths and LLTrans after LLUI::initClass has been called.
-	LLUI::setupPaths();
+	// NOW LLUI::getLanguage() should work. gDirUtilp must know the language
+	// for this session ASAP so all the file-loading commands that follow,
+	// that use findSkinnedFilenames(), will include the localized files.
+	gDirUtilp->setSkinFolder(gDirUtilp->getSkinFolder(), LLUI::getLanguage());
+
+	// Setup LLTrans after LLUI::initClass has been called.
 	LLTransUtil::parseStrings("strings.xml", default_trans_args);
 	LLTransUtil::parseLanguageStrings("language_settings.xml");
 
-	// Setup notifications after LLUI::setupPaths() has been called.
+	// Setup notifications after LLUI::initClass() has been called.
 	LLNotifications::instance();
 	LL_INFOS("InitInfo") << "Notifications initialized." << LL_ENDL ;
 
 		OSMessageBox(msg.str(),LLStringUtil::null,OSMB_OK);
 		return false;
 	}
-	
-	LLUI::setupPaths(); // setup paths for LLTrans based on settings files only
+
 	LLTransUtil::parseStrings("strings.xml", default_trans_args);
 	LLTransUtil::parseLanguageStrings("language_settings.xml");
 	// - set procedural settings
 		LLStartUp::setStartSLURL(start_slurl);
     }
 
-    const LLControlVariable* skinfolder = gSavedSettings.getControl("SkinCurrent");
-    if(skinfolder && LLStringUtil::null != skinfolder->getValue().asString())
-    {   
-		// hack to force the skin to default.
-        gDirUtilp->setSkinFolder(skinfolder->getValue().asString());
-		//gDirUtilp->setSkinFolder("default");
-    }
+	const LLControlVariable* skinfolder = gSavedSettings.getControl("SkinCurrent");
+	if(skinfolder && LLStringUtil::null != skinfolder->getValue().asString())
+	{	
+		// Examining "Language" may not suffice -- see LLUI::getLanguage()
+		// logic. Unfortunately LLUI::getLanguage() doesn't yet do us much
+		// good because we haven't yet called LLUI::initClass().
+		gDirUtilp->setSkinFolder(skinfolder->getValue().asString(),
+								 gSavedSettings.getString("Language"));
+	}
 
 	if (gSavedSettings.getBOOL("SpellCheck"))
 	{
 	{
 		gSavedSettings.setBOOL("MigrateCacheDirectory", FALSE);
 
-		std::string delimiter = gDirUtilp->getDirDelimiter();
-		std::string old_cache_dir = gDirUtilp->getOSUserAppDir() + delimiter + "cache";
+		std::string old_cache_dir = gDirUtilp->add(gDirUtilp->getOSUserAppDir(), "cache");
 		std::string new_cache_dir = gDirUtilp->getCacheDir(true);
 
 		if (gDirUtilp->fileExists(old_cache_dir))
 			while (iter.next(file_name))
 			{
 				if (file_name == "." || file_name == "..") continue;
-				std::string source_path = old_cache_dir + delimiter + file_name;
-				std::string dest_path = new_cache_dir + delimiter + file_name;
+				std::string source_path = gDirUtilp->add(old_cache_dir, file_name);
+				std::string dest_path = gDirUtilp->add(new_cache_dir, file_name);
 				if (!LLFile::rename(source_path, dest_path))
 				{
 					file_count++;
 		LLDirIterator iter(dir, mask);
 		if (iter.next(found_file))
 		{
-			old_vfs_data_file = dir + gDirUtilp->getDirDelimiter() + found_file;
+			old_vfs_data_file = gDirUtilp->add(dir, found_file);
 
 			S32 start_pos = found_file.find_last_of('.');
 			if (start_pos > 0)
 	// we tell the updater where to find the xml containing string
 	// translations which it can use for its own UI
 	std::string xml_strings_file = "strings.xml";
-	std::vector<std::string> xui_path_vec = LLUI::getXUIPaths();
+	std::vector<std::string> xui_path_vec =
+		gDirUtilp->findSkinnedFilenames(LLDir::XUI, xml_strings_file);
 	std::string xml_search_paths;
-	std::vector<std::string>::const_iterator iter;
+	const char* delim = "";
 	// build comma-delimited list of xml paths to pass to updater
-	for (iter = xui_path_vec.begin(); iter != xui_path_vec.end(); )
-	{
-		std::string this_skin_dir = gDirUtilp->getDefaultSkinDir()
-			+ gDirUtilp->getDirDelimiter()
-			+ (*iter);
-		llinfos << "Got a XUI path: " << this_skin_dir << llendl;
-		xml_search_paths.append(this_skin_dir);
-		++iter;
-		if (iter != xui_path_vec.end())
-			xml_search_paths.append(","); // comma-delimit
+	BOOST_FOREACH(std::string this_skin_path, xui_path_vec)
+	{
+		// Although we already have the full set of paths with the filename
+		// appended, the linux-updater.bin command-line switches require us to
+		// snip the filename OFF and pass it as a separate switch argument. :-P
+		llinfos << "Got a XUI path: " << this_skin_path << llendl;
+		xml_search_paths.append(delim);
+		xml_search_paths.append(gDirUtilp->getDirName(this_skin_path));
+		delim = ",";
 	}
 	// build the overall command-line to run the updater correctly
 	LLAppViewer::sUpdaterInfo->mUpdateExePath = 

indra/newview/lldaycyclemanager.cpp

 	{
 		std::string file;
 		if (!dir_iter.next(file)) break; // no more files
-		loadPreset(dir + file);
+		loadPreset(gDirUtilp->add(dir, file));
 	}
 }
 

indra/newview/llfloateruipreview.cpp

 	virtual ~LLFloaterUIPreview();
 
 	std::string getLocStr(S32 ID);							// fetches the localization string based on what is selected in the drop-down menu
-	void displayFloater(BOOL click, S32 ID, bool save = false);			// needs to be public so live file can call it when it finds an update
+	void displayFloater(BOOL click, S32 ID);			// needs to be public so live file can call it when it finds an update
 
 	/*virtual*/ BOOL postBuild();
 	/*virtual*/ void onClose(bool app_quitting);
 {
 	mSavedLocalization = LLUI::sSettingGroups["config"]->getString("Language");				// save current localization setting
 	LLUI::sSettingGroups["config"]->setString("Language", floater->getLocStr(ID));// hack language to be the one we want to preview floaters in
-	LLUI::setupPaths();														// forcibly reset XUI paths with this new language
+	// forcibly reset XUI paths with this new language
+	gDirUtilp->setSkinFolder(gDirUtilp->getSkinFolder(), floater->getLocStr(ID));
 }
 
 // Actually reset in destructor
 LLLocalizationResetForcer::~LLLocalizationResetForcer()
 {
 	LLUI::sSettingGroups["config"]->setString("Language", mSavedLocalization);	// reset language to what it was before we changed it
-	LLUI::setupPaths();														// forcibly reset XUI paths with this new language
+	// forcibly reset XUI paths with this new language
+	gDirUtilp->setSkinFolder(gDirUtilp->getSkinFolder(), mSavedLocalization);
 }
 
 // Live file constructor
 	{
 		if((found = iter.next(language_directory)))							// get next directory
 		{
-			std::string full_path = xui_dir + language_directory;
+			std::string full_path = gDirUtilp->add(xui_dir, language_directory);
 			if(LLFile::isfile(full_path.c_str()))																	// if it's not a directory, skip it
 			{
 				continue;
 // Saves the current floater/panel
 void LLFloaterUIPreview::onClickSaveFloater(S32 caller_id)
 {
-	displayFloater(TRUE, caller_id, true);
+	displayFloater(TRUE, caller_id);
+	popupAndPrintWarning("Save-floater functionality removed, use XML schema to clean up XUI files");
 }
 
 // Saves all floater/panels
 	for (int index = 0; index < listSize; index++)
 	{
 		mFileList->selectNthItem(index);
-		displayFloater(TRUE, caller_id, true);
+		displayFloater(TRUE, caller_id);
 	}
-}
-
-// Given path to floater or panel XML file "filename.xml",
-// returns "filename_new.xml"
-static std::string append_new_to_xml_filename(const std::string& path)
-{
-	std::string full_filename = gDirUtilp->findSkinnedFilename(LLUI::getLocalizedSkinPath(), path);
-	std::string::size_type extension_pos = full_filename.rfind(".xml");
-	full_filename.resize(extension_pos);
-	full_filename += "_new.xml";
-	return full_filename;
+	popupAndPrintWarning("Save-floater functionality removed, use XML schema to clean up XUI files");
 }
 
 // Actually display the floater
 // Only set up a new live file if this came from a click (at which point there should be no existing live file), rather than from the live file's update itself;
 // otherwise, we get an infinite loop as the live file keeps recreating itself.  That means this function is generally called twice.
-void LLFloaterUIPreview::displayFloater(BOOL click, S32 ID, bool save)
+void LLFloaterUIPreview::displayFloater(BOOL click, S32 ID)
 {
 	// Convince UI that we're in a different language (the one selected on the drop-down menu)
 	LLLocalizationResetForcer reset_forcer(this, ID);						// save old language in reset forcer object (to be reset upon destruction when it falls out of scope)
 	if(!strncmp(path.c_str(),"floater_",8)
 		|| !strncmp(path.c_str(), "i