From 3653727e7f84f10caefb6ea7dc33859455ebfa0b Mon Sep 17 00:00:00 2001
From: Nat Goodspeed <nat@lindenlab.com>
Date: Wed, 10 Oct 2012 14:57:43 -0400
Subject: [PATCH] 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.

---
 .../llimage_libtest/llimage_libtest.cpp       |   2 +-
 .../llui_libtest/llui_libtest.cpp             |  29 +-
 indra/linux_updater/linux_updater.cpp         |   2 +-
 indra/llrender/llfontgl.cpp                   |   4 +-
 indra/llrender/llfontgl.h                     |   2 +-
 indra/llrender/llfontregistry.cpp             |  30 +-
 indra/llrender/llfontregistry.h               |   4 +-
 indra/llui/llfloater.cpp                      |  18 +-
 indra/llui/llfloater.h                        |   2 +-
 indra/llui/llfloaterreg.cpp                   |   2 +-
 indra/llui/llnotifications.cpp                |  25 +-
 indra/llui/llpanel.cpp                        |  18 +-
 indra/llui/llpanel.h                          |   2 +-
 indra/llui/llui.cpp                           |  87 +---
 indra/llui/llui.h                             |   5 -
 indra/llui/lluicolortable.cpp                 |  17 +-
 indra/llui/lluictrlfactory.cpp                |  46 +-
 indra/llui/lluictrlfactory.h                  |  17 +-
 indra/llvfs/lldir.cpp                         | 458 ++++++++++++++----
 indra/llvfs/lldir.h                           |  91 +++-
 indra/llvfs/tests/lldir_test.cpp              | 339 ++++++++++++-
 indra/newview/llappviewer.cpp                 |  65 +--
 indra/newview/lldaycyclemanager.cpp           |   2 +-
 indra/newview/llfloateruipreview.cpp          | 122 ++---
 indra/newview/llhints.cpp                     |   4 +-
 indra/newview/llmediactrl.cpp                 |  30 +-
 indra/newview/llpreviewscript.cpp             |   2 +-
 indra/newview/llsyswellwindow.cpp             |   4 +-
 indra/newview/lltoast.cpp                     |   2 +-
 indra/newview/llviewermedia.cpp               |  15 +-
 indra/newview/llviewertexturelist.cpp         |  55 +--
 indra/newview/llviewerwindow.cpp              |   6 +-
 indra/newview/llwaterparammanager.cpp         |   2 +-
 indra/newview/llwlparammanager.cpp            |   2 +-
 indra/newview/skins/paths.xml                 |  10 -
 indra/newview/tests/lldir_stub.cpp            |   2 +-
 indra/newview/viewer_manifest.py              |   1 -
 .../updater/tests/llupdaterservice_test.cpp   |   4 +-
 38 files changed, 978 insertions(+), 550 deletions(-)
 delete mode 100644 indra/newview/skins/paths.xml

diff --git a/indra/integration_tests/llimage_libtest/llimage_libtest.cpp b/indra/integration_tests/llimage_libtest/llimage_libtest.cpp
index 36c5b678261..034c816742b 100644
--- a/indra/integration_tests/llimage_libtest/llimage_libtest.cpp
+++ b/indra/integration_tests/llimage_libtest/llimage_libtest.cpp
@@ -240,7 +240,7 @@ void store_input_file(std::list<std::string> &input_filenames, const std::string
 		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);
 		}
 	}
diff --git a/indra/integration_tests/llui_libtest/llui_libtest.cpp b/indra/integration_tests/llui_libtest/llui_libtest.cpp
index 217e26c3ca5..38aa1bbeb2a 100644
--- a/indra/integration_tests/llui_libtest/llui_libtest.cpp
+++ b/indra/integration_tests/llui_libtest/llui_libtest.cpp
@@ -107,12 +107,6 @@ class TestImageProvider : public LLImageProviderInterface
 };
 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
@@ -122,13 +116,12 @@ void init_llui()
 	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()
@@ -143,9 +136,7 @@ void init_llui()
 	
 	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;
@@ -157,7 +148,6 @@ void init_llui()
 	// otherwise it crashes.
 	LLFontGL::initClass(96.f, 1.f, 1.f,
 						gDirUtilp->getAppRODataDir(),
-						LLUI::getXUIPaths(),
 						false );	// don't create gl textures
 	
 	LLFloaterView::Params fvparams;
@@ -169,6 +159,14 @@ void init_llui()
 	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
@@ -191,7 +189,7 @@ void export_test_floaters()
 		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";
@@ -203,6 +201,7 @@ void export_test_floaters()
 		fclose(floater_file);
 	}
 }
+|*==========================================================================*/
 
 int main(int argc, char** argv)
 {
@@ -211,7 +210,7 @@ int main(int argc, char** argv)
 
 	init_llui();
 	
-	export_test_floaters();
+//	export_test_floaters();
 	
 	return 0;
 }
diff --git a/indra/linux_updater/linux_updater.cpp b/indra/linux_updater/linux_updater.cpp
index 277f0a5367a..991dfd9dce5 100644
--- a/indra/linux_updater/linux_updater.cpp
+++ b/indra/linux_updater/linux_updater.cpp
@@ -251,7 +251,7 @@ std::string next_image_filename(std::string& image_path, LLDirIterator& iter)
 {
 	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)
diff --git a/indra/llrender/llfontgl.cpp b/indra/llrender/llfontgl.cpp
index 4dc2fcd7146..647512eb2ed 100644
--- a/indra/llrender/llfontgl.cpp
+++ b/indra/llrender/llfontgl.cpp
@@ -789,7 +789,7 @@ const LLFontDescriptor& LLFontGL::getFontDesc() const
 }
 
 // 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);
@@ -800,7 +800,7 @@ void LLFontGL::initClass(F32 screen_dpi, F32 x_scale, F32 y_scale, const std::st
 	// Font registry init
 	if (!sFontRegistry)
 	{
-		sFontRegistry = new LLFontRegistry(xui_paths, create_gl_textures);
+		sFontRegistry = new LLFontRegistry(create_gl_textures);
 		sFontRegistry->parseFontInfo("fonts.xml");
 	}
 	else
diff --git a/indra/llrender/llfontgl.h b/indra/llrender/llfontgl.h
index 5ed5d2c4ebe..0988e99deb3 100644
--- a/indra/llrender/llfontgl.h
+++ b/indra/llrender/llfontgl.h
@@ -150,7 +150,7 @@ class LLFontGL
 	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.
diff --git a/indra/llrender/llfontregistry.cpp b/indra/llrender/llfontregistry.cpp
index 4d22eba3d97..b5bdba996ff 100644
--- a/indra/llrender/llfontregistry.cpp
+++ b/indra/llrender/llfontregistry.cpp
@@ -163,14 +163,9 @@ LLFontDescriptor LLFontDescriptor::normalize() const
 	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();
@@ -183,27 +178,30 @@ LLFontRegistry::~LLFontRegistry()
 
 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"))
@@ -215,7 +213,7 @@ bool LLFontRegistry::parseFontInfo(const std::string& xml_filename)
 	}
 	//if (success)
 	//	dump();
-	
+
 	return success;
 }
 
diff --git a/indra/llrender/llfontregistry.h b/indra/llrender/llfontregistry.h
index 8b06191c564..059248fbbdb 100644
--- a/indra/llrender/llfontregistry.h
+++ b/indra/llrender/llfontregistry.h
@@ -67,8 +67,7 @@ class LLFontRegistry
 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).
@@ -105,7 +104,6 @@ class LLFontRegistry
 	font_size_map_t mFontSizes;
 
 	string_vec_t mUltimateFallbackList;
-	string_vec_t mXUIPaths;
 	bool mCreateGLTextures;
 };
 
diff --git a/indra/llui/llfloater.cpp b/indra/llui/llfloater.cpp
index 8ca1e685a96..33295f882d6 100644
--- a/indra/llui/llfloater.cpp
+++ b/indra/llui/llfloater.cpp
@@ -3229,24 +3229,14 @@ bool LLFloater::isVisible(const LLFloater* floater)
 
 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::getLocalizedXMLNode(filename, root))
-		{
-			llwarns << "Couldn't parse floater from: " << LLUI::getLocalizedSkinPath() + gDirUtilp->getDirDelimiter() + filename << llendl;
-			return false;
-		}
-	}
-	else if (!LLUICtrlFactory::getLayeredXMLNode(filename, root))
+	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;
 	}
 	
@@ -3271,7 +3261,7 @@ bool LLFloater::buildFromFile(const std::string& filename, LLXMLNodePtr output_n
 		getCommitCallbackRegistrar().pushScope();
 		getEnableCallbackRegistrar().pushScope();
 		
-		res = initFloaterXML(root, getParent(), filename, output_node);
+		res = initFloaterXML(root, getParent(), filename, NULL);
 
 		setXMLFilename(filename);
 		
diff --git a/indra/llui/llfloater.h b/indra/llui/llfloater.h
index 64d6dcea044..e64b6d04d3f 100644
--- a/indra/llui/llfloater.h
+++ b/indra/llui/llfloater.h
@@ -202,7 +202,7 @@ class LLFloater : public LLPanel, public LLInstanceTracker<LLFloater>
 
 	// 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 );
diff --git a/indra/llui/llfloaterreg.cpp b/indra/llui/llfloaterreg.cpp
index 9115eb71740..306caf2b91b 100644
--- a/indra/llui/llfloaterreg.cpp
+++ b/indra/llui/llfloaterreg.cpp
@@ -154,7 +154,7 @@ LLFloater* LLFloaterReg::getInstance(const std::string& name, const LLSD& key)
 					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;
diff --git a/indra/llui/llnotifications.cpp b/indra/llui/llnotifications.cpp
index 629eef2c3bc..4fbee8cd800 100644
--- a/indra/llui/llnotifications.cpp
+++ b/indra/llui/llnotifications.cpp
@@ -1424,25 +1424,18 @@ void addPathIfExists(const std::string& new_path, std::vector<std::string>& path
 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;
 	}
 
@@ -1452,7 +1445,7 @@ bool LLNotifications::loadTemplates()
 
 	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;
 	}
 
@@ -1508,7 +1501,9 @@ bool LLNotifications::loadTemplates()
 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;
diff --git a/indra/llui/llpanel.cpp b/indra/llui/llpanel.cpp
index 00318cec6b0..67472ad1666 100644
--- a/indra/llui/llpanel.cpp
+++ b/indra/llui/llpanel.cpp
@@ -968,25 +968,15 @@ static LLFastTimer::DeclareTimer FTM_BUILD_PANELS("Build Panels");
 //-----------------------------------------------------------------------------
 // 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;
 	}
 
@@ -1010,7 +1000,7 @@ BOOL LLPanel::buildFromFile(const std::string& filename, LLXMLNodePtr output_nod
 		getCommitCallbackRegistrar().pushScope();
 		getEnableCallbackRegistrar().pushScope();
 		
-		didPost = initPanelXML(root, NULL, output_node, default_params);
+		didPost = initPanelXML(root, NULL, NULL, default_params);
 
 		getCommitCallbackRegistrar().popScope();
 		getEnableCallbackRegistrar().popScope();
diff --git a/indra/llui/llpanel.h b/indra/llui/llpanel.h
index f6202010202..e63b41f97c5 100644
--- a/indra/llui/llpanel.h
+++ b/indra/llui/llpanel.h
@@ -105,7 +105,7 @@ class LLPanel : public LLUICtrl, public LLBadgeHolder
 	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);
 
diff --git a/indra/llui/llui.cpp b/indra/llui/llui.cpp
index 87bf518aa17..507ced91725 100644
--- a/indra/llui/llui.cpp
+++ b/indra/llui/llui.cpp
@@ -1836,88 +1836,39 @@ struct Paths : public LLInitParam::Block<Paths>
 	{}
 };
 
-//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()
diff --git a/indra/llui/llui.h b/indra/llui/llui.h
index 28e84fa4441..c5a12d2b315 100644
--- a/indra/llui/llui.h
+++ b/indra/llui/llui.h
@@ -292,11 +292,6 @@ class LLUI
 	// 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; }
diff --git a/indra/llui/lluicolortable.cpp b/indra/llui/lluicolortable.cpp
index 9455d09cc0c..27174453965 100644
--- a/indra/llui/lluicolortable.cpp
+++ b/indra/llui/lluicolortable.cpp
@@ -32,6 +32,7 @@
 #include "llui.h"
 #include "lluicolortable.h"
 #include "lluictrlfactory.h"
+#include <boost/foreach.hpp>
 
 LLUIColorTable::ColorParams::ColorParams()
 :	value("value"),
@@ -206,19 +207,11 @@ bool LLUIColorTable::loadFromSettings()
 {
 	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)
-	{
-		result |= loadFromFilename(current_filename, mLoadedColors);
-	}
-
-	current_filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_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);
+		result |= loadFromFilename(colors_path, mLoadedColors);
 	}
 
 	std::string user_filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "colors.xml");
diff --git a/indra/llui/lluictrlfactory.cpp b/indra/llui/lluictrlfactory.cpp
index 25e7a31e907..2b317b46e32 100644
--- a/indra/llui/lluictrlfactory.cpp
+++ b/indra/llui/lluictrlfactory.cpp
@@ -90,10 +90,12 @@ LLUICtrlFactory::~LLUICtrlFactory()
 
 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);
@@ -152,19 +154,8 @@ static LLFastTimer::DeclareTimer FTM_XML_PARSE("XML Reading/Parsing");
 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())
 	{
@@ -176,23 +167,6 @@ bool LLUICtrlFactory::getLayeredXMLNode(const std::string &xui_filename, LLXMLNo
 }
 
 
-//-----------------------------------------------------------------------------
-// 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()
 //-----------------------------------------------------------------------------
@@ -239,8 +213,10 @@ std::string LLUICtrlFactory::getCurFileName()
 
 
 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() 
@@ -260,7 +236,7 @@ void LLUICtrlFactory::setCtrlParent(LLView* view, LLView* parent, S32 tab_group)
 //static
 std::string LLUICtrlFactory::findSkinnedFilename(const std::string& filename)
 {
-	return gDirUtilp->findSkinnedFilename(LLUI::getSkinPath(), filename);
+	return gDirUtilp->findSkinnedFilenameBaseLang(LLDir::XUI, filename);
 }
 
 //static 
diff --git a/indra/llui/lluictrlfactory.h b/indra/llui/lluictrlfactory.h
index d612ad5005d..56e5f3eb7b6 100644
--- a/indra/llui/lluictrlfactory.h
+++ b/indra/llui/lluictrlfactory.h
@@ -169,7 +169,7 @@ class LLUICtrlFactory : public LLSingleton<LLUICtrlFactory>
 	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;
 		
@@ -178,23 +178,13 @@ class LLUICtrlFactory : public LLSingleton<LLUICtrlFactory>
 		{
 			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);
@@ -223,7 +213,6 @@ class LLUICtrlFactory : public LLSingleton<LLUICtrlFactory>
 	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
diff --git a/indra/llvfs/lldir.cpp b/indra/llvfs/lldir.cpp
index 32d081d5524..a7d12476a45 100644
--- a/indra/llvfs/lldir.cpp
+++ b/indra/llvfs/lldir.cpp
@@ -41,6 +41,12 @@
 #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"
@@ -58,6 +64,14 @@ LLDir_Linux gDirUtil;
 
 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(""),
@@ -70,7 +84,8 @@ LLDir::LLDir()
 	mOSCacheDir(""),
 	mCAFile(""),
 	mTempDir(""),
-	mDirDelimiter("/") // fallback to forward slash if not overridden
+	mDirDelimiter("/"), // fallback to forward slash if not overridden
+	mLanguage("en")
 {
 }
 
@@ -96,9 +111,7 @@ S32 LLDir::deleteFilesInDir(const std::string &dirname, const std::string &mask)
 	LLDirIterator iter(dirname, mask);
 	while (iter.next(filename))
 	{
-		fullpath = dirname;
-		fullpath += getDirDelimiter();
-		fullpath += filename;
+		fullpath = add(dirname, filename);
 
 		if(LLFile::isdir(fullpath))
 		{
@@ -270,12 +283,12 @@ std::string LLDir::buildSLOSCacheDir() const
 		}
 		else
 		{
-			res = getOSUserAppDir() + mDirDelimiter + "cache";
+			res = add(getOSUserAppDir(), "cache");
 		}
 	}
 	else
 	{
-		res = getOSCacheDir() + mDirDelimiter + "SecondLife";
+		res = add(getOSCacheDir(), "SecondLife");
 	}
 	return res;
 }
@@ -298,19 +311,24 @@ const std::string &LLDir::getDirDelimiter() const
 	return mDirDelimiter;
 }
 
+const std::string& LLDir::getDefaultSkinDir() const
+{
+	return mDefaultSkinDir;
+}
+
 const std::string &LLDir::getSkinDir() const
 {
 	return mSkinDir;
 }
 
-const std::string &LLDir::getUserSkinDir() const
+const std::string &LLDir::getUserDefaultSkinDir() const
 {
-	return mUserSkinDir;
+    return mUserDefaultSkinDir;
 }
 
-const std::string& LLDir::getDefaultSkinDir() const
+const std::string &LLDir::getUserSkinDir() const
 {
-	return mDefaultSkinDir;
+	return mUserSkinDir;
 }
 
 const std::string LLDir::getSkinBaseDir() const
@@ -323,6 +341,41 @@ const std::string &LLDir::getLLPluginDir() const
 	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);
@@ -343,15 +396,11 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd
 		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:
@@ -363,16 +412,22 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd
 		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;
@@ -386,9 +441,7 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd
 		break;
 
 	case LL_PATH_LOGS:
-		prefix = getOSUserAppDir();
-		prefix += mDirDelimiter;
-		prefix += "logs";
+		prefix = add(getOSUserAppDir(), "logs");
 		break;
 
 	case LL_PATH_TEMP:
@@ -412,9 +465,7 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd
 		break;
 
 	case LL_PATH_LOCAL_ASSETS:
-		prefix = getAppRODataDir();
-		prefix += mDirDelimiter;
-		prefix += "local_assets";
+		prefix = add(getAppRODataDir(), "local_assets");
 		break;
 
 	case LL_PATH_EXECUTABLE:
@@ -422,56 +473,36 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd
 		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())
-	{
-		filename = subdir2 + mDirDelimiter + filename;
-	}
-
-	if (!subdir1.empty())
-	{
-		filename = subdir1 + mDirDelimiter + 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;
+		llwarns << ELLPathToString(location)
+				<< ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename
+				<< "': prefix is empty, possible bad filename" << llendl;
 	}
-	else
+
+	std::string expanded_filename = add(add(prefix, subdir1), subdir2);
+	if (expanded_filename.empty() && in_filename.empty())
 	{
-		expanded_filename.assign("");
+		return "";
 	}
-
-	//llinfos << "*** EXPANDED FILENAME: <" << expanded_filename << ">" << llendl;
+	// 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;
+
+	LL_DEBUGS("LLDir") << ELLPathToString(location)
+					   << ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename
+					   << "' => '" << expanded_filename << "'" << LL_ENDL;
 	return expanded_filename;
 }
 
@@ -511,31 +542,168 @@ std::string LLDir::getExtension(const std::string& filepath) const
 	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));
+
+	LL_DEBUGS("LLDir") << "subdir '" << subdir << "', filename '" << filename
+					   << "', merge " << std::boolalpha << merge << LL_ENDL;
+
+	// 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;
 
-	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
+		// 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);
+			}
+		}
+	}
 
-	std::string found_file = findFile(filename, search_paths);
-	return found_file;
+	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
@@ -546,12 +714,7 @@ 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
@@ -587,9 +750,7 @@ void LLDir::setLindenUserDir(const std::string &username)
 		std::string userlower(username);
 		LLStringUtil::toLower(userlower);
 		LLStringUtil::replaceChar(userlower, ' ', '_');
-		mLindenUserDir = getOSUserAppDir();
-		mLindenUserDir += mDirDelimiter;
-		mLindenUserDir += userlower;
+		mLindenUserDir = add(getOSUserAppDir(), userlower);
 	}
 	else
 	{
@@ -621,9 +782,7 @@ void LLDir::setPerAccountChatLogsDir(const std::string &username)
 		std::string userlower(username);
 		LLStringUtil::toLower(userlower);
 		LLStringUtil::replaceChar(userlower, ' ', '_');
-		mPerAccountChatLogsDir = getChatLogsDir();
-		mPerAccountChatLogsDir += mDirDelimiter;
-		mPerAccountChatLogsDir += userlower;
+		mPerAccountChatLogsDir = add(getChatLogsDir(), userlower);
 	}
 	else
 	{
@@ -632,25 +791,59 @@ void LLDir::setPerAccountChatLogsDir(const std::string &username)
 	
 }
 
-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)
@@ -664,7 +857,7 @@ 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)
 		{
@@ -697,6 +890,57 @@ void LLDir::dumpCurrentDirectories()
 	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)
 {
diff --git a/indra/llvfs/lldir.h b/indra/llvfs/lldir.h
index 5ee8bdb542d..a242802979a 100644
--- a/indra/llvfs/lldir.h
+++ b/indra/llvfs/lldir.h
@@ -56,7 +56,7 @@ typedef enum ELLPath
 	LL_PATH_LAST
 } ELLPath;
 
-
+/// Directory operations
 class LLDir
 {
  public:
@@ -100,9 +100,10 @@ class LLDir
 	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
 
@@ -117,10 +118,59 @@ class LLDir
 	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;
@@ -132,15 +182,30 @@ class LLDir
 	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
@@ -158,10 +223,18 @@ class LLDir
 	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 mSkinDir;			// Location for current skin info.
 	std::string mDefaultSkinDir;			// Location for default skin info.
+	std::string mSkinDir;			// Location for current 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
 };
 
diff --git a/indra/llvfs/tests/lldir_test.cpp b/indra/llvfs/tests/lldir_test.cpp
index ea321c5ae95..a00fc8684ca 100644
--- a/indra/llvfs/tests/lldir_test.cpp
+++ b/indra/llvfs/tests/lldir_test.cpp
@@ -27,11 +27,161 @@
 
 #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
 {
@@ -419,5 +569,192 @@ 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");
+    }
+}
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index e8934d9a9ed..e7a8a52a755 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -122,7 +122,6 @@
 #include <boost/algorithm/string.hpp>
 
 
-
 #if LL_WINDOWS
 #	include <share.h> // For _SH_DENYWR in initMarkerFile
 #else
@@ -684,7 +683,7 @@ bool LLAppViewer::init()
 	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();
 	
@@ -768,12 +767,16 @@ bool LLAppViewer::init()
 		&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 ;
 
@@ -2242,8 +2245,7 @@ bool LLAppViewer::initConfiguration()
 		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
@@ -2559,13 +2561,15 @@ bool LLAppViewer::initConfiguration()
 		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"))
 	{
@@ -3589,8 +3593,7 @@ void LLAppViewer::migrateCacheDirectory()
 	{
 		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))
@@ -3606,8 +3609,8 @@ void LLAppViewer::migrateCacheDirectory()
 			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++;
@@ -3838,7 +3841,7 @@ bool LLAppViewer::initCache()
 		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)
@@ -5149,20 +5152,20 @@ void LLAppViewer::launchUpdater()
 	// 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 = 
diff --git a/indra/newview/lldaycyclemanager.cpp b/indra/newview/lldaycyclemanager.cpp
index 347a467a8bf..8af2f4ea336 100644
--- a/indra/newview/lldaycyclemanager.cpp
+++ b/indra/newview/lldaycyclemanager.cpp
@@ -184,7 +184,7 @@ void LLDayCycleManager::loadPresets(const std::string& dir)
 	{
 		std::string file;
 		if (!dir_iter.next(file)) break; // no more files
-		loadPreset(dir + file);
+		loadPreset(gDirUtilp->add(dir, file));
 	}
 }
 
diff --git a/indra/newview/llfloateruipreview.cpp b/indra/newview/llfloateruipreview.cpp
index d741b5b1335..15e0b89f6cc 100644
--- a/indra/newview/llfloateruipreview.cpp
+++ b/indra/newview/llfloateruipreview.cpp
@@ -137,7 +137,7 @@ class LLFloaterUIPreview : public LLFloater
 	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);
@@ -291,7 +291,8 @@ LLLocalizationResetForcer::LLLocalizationResetForcer(LLFloaterUIPreview* floater
 {
 	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
@@ -299,7 +300,8 @@ LLLocalizationResetForcer::LLLocalizationResetForcer(LLFloaterUIPreview* floater
 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
@@ -488,7 +490,7 @@ BOOL LLFloaterUIPreview::postBuild()
 	{
 		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;
@@ -773,7 +775,8 @@ void LLFloaterUIPreview::onClickDisplayFloater(S32 caller_id)
 // 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
@@ -784,25 +787,15 @@ void LLFloaterUIPreview::onClickSaveAll(S32 caller_id)
 	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)
@@ -843,48 +836,13 @@ void LLFloaterUIPreview::displayFloater(BOOL click, S32 ID, bool save)
 	if(!strncmp(path.c_str(),"floater_",8)
 		|| !strncmp(path.c_str(), "inspect_", 8))		// if it's a floater
 	{
-		if (save)
-		{
-			LLXMLNodePtr floater_write = new LLXMLNode();			
-			(*floaterp)->buildFromFile(path, floater_write);	// just build it
-
-			if (!floater_write->isNull())
-			{
-				std::string full_filename = append_new_to_xml_filename(path);
-				LLFILE* floater_temp = LLFile::fopen(full_filename.c_str(), "w");
-				LLXMLNode::writeHeaderToFile(floater_temp);
-				const bool use_type_decorations = false;
-				floater_write->writeToFile(floater_temp, std::string(), use_type_decorations);
-				fclose(floater_temp);
-			}
-		}
-		else
-		{
-			(*floaterp)->buildFromFile(path);	// just build it
-			(*floaterp)->openFloater((*floaterp)->getKey());
-			(*floaterp)->setCanResize((*floaterp)->isResizable());
-		}
-
+		(*floaterp)->buildFromFile(path);	// just build it
+		(*floaterp)->openFloater((*floaterp)->getKey());
+		(*floaterp)->setCanResize((*floaterp)->isResizable());
 	}
 	else if (!strncmp(path.c_str(),"menu_",5))								// if it's a menu
 	{
-		if (save)
-		{	
-			LLXMLNodePtr menu_write = new LLXMLNode();	
-			LLMenuGL* menu = LLUICtrlFactory::getInstance()->createFromFile<LLMenuGL>(path, gMenuHolder, LLViewerMenuHolderGL::child_registry_t::instance(), menu_write);
-
-			if (!menu_write->isNull())
-			{
-				std::string full_filename = append_new_to_xml_filename(path);
-				LLFILE* menu_temp = LLFile::fopen(full_filename.c_str(), "w");
-				LLXMLNode::writeHeaderToFile(menu_temp);
-				const bool use_type_decorations = false;
-				menu_write->writeToFile(menu_temp, std::string(), use_type_decorations);
-				fclose(menu_temp);
-			}
-
-			delete menu;
-		}
+		// former 'save' processing excised
 	}
 	else																// if it is a panel...
 	{
@@ -896,39 +854,21 @@ void LLFloaterUIPreview::displayFloater(BOOL click, S32 ID, bool save)
 		LLPanel::Params panel_params;
 		LLPanel* panel = LLUICtrlFactory::create<LLPanel>(panel_params);	// create a new panel
 
-		if (save)
-		{
-			LLXMLNodePtr panel_write = new LLXMLNode();
-			panel->buildFromFile(path, panel_write);		// build it
-			
-			if (!panel_write->isNull())
-			{
-				std::string full_filename = append_new_to_xml_filename(path);
-				LLFILE* panel_temp = LLFile::fopen(full_filename.c_str(), "w");
-				LLXMLNode::writeHeaderToFile(panel_temp);
-				const bool use_type_decorations = false;
-				panel_write->writeToFile(panel_temp, std::string(), use_type_decorations);
-				fclose(panel_temp);
-			}
-		}
-		else
-		{
-			panel->buildFromFile(path);										// build it
-			LLRect new_size = panel->getRect();								// get its rectangle
-			panel->setOrigin(2,2);											// reset its origin point so it's not offset by -left or other XUI attributes
-			(*floaterp)->setTitle(path);									// use the file name as its title, since panels have no guaranteed meaningful name attribute
-			panel->setUseBoundingRect(TRUE);								// enable the use of its outer bounding rect (normally disabled because it's O(n) on the number of sub-elements)
-			panel->updateBoundingRect();									// update bounding rect
-			LLRect bounding_rect = panel->getBoundingRect();				// get the bounding rect
-			LLRect new_rect = panel->getRect();								// get the panel's rect
-			new_rect.unionWith(bounding_rect);								// union them to make sure we get the biggest one possible
-			LLRect floater_rect = new_rect;
-			floater_rect.stretch(4, 4);
-			(*floaterp)->reshape(floater_rect.getWidth(), floater_rect.getHeight() + floater_header_size);	// reshape floater to match the union rect's dimensions
-			panel->reshape(new_rect.getWidth(), new_rect.getHeight());		// reshape panel to match the union rect's dimensions as well (both are needed)
-			(*floaterp)->addChild(panel);					// add panel as child
-			(*floaterp)->openFloater();						// open floater (needed?)
-		}
+		panel->buildFromFile(path);										// build it
+		LLRect new_size = panel->getRect();								// get its rectangle
+		panel->setOrigin(2,2);											// reset its origin point so it's not offset by -left or other XUI attributes
+		(*floaterp)->setTitle(path);									// use the file name as its title, since panels have no guaranteed meaningful name attribute
+		panel->setUseBoundingRect(TRUE);								// enable the use of its outer bounding rect (normally disabled because it's O(n) on the number of sub-elements)
+		panel->updateBoundingRect();									// update bounding rect
+		LLRect bounding_rect = panel->getBoundingRect();				// get the bounding rect
+		LLRect new_rect = panel->getRect();								// get the panel's rect
+		new_rect.unionWith(bounding_rect);								// union them to make sure we get the biggest one possible
+		LLRect floater_rect = new_rect;
+		floater_rect.stretch(4, 4);
+		(*floaterp)->reshape(floater_rect.getWidth(), floater_rect.getHeight() + floater_header_size);	// reshape floater to match the union rect's dimensions
+		panel->reshape(new_rect.getWidth(), new_rect.getHeight());		// reshape panel to match the union rect's dimensions as well (both are needed)
+		(*floaterp)->addChild(panel);					// add panel as child
+		(*floaterp)->openFloater();						// open floater (needed?)
 	}
 
 	if(ID == 1)
@@ -964,7 +904,7 @@ void LLFloaterUIPreview::displayFloater(BOOL click, S32 ID, bool save)
 	(*floaterp)->center();
 	addDependentFloater(*floaterp);
 
-	if(click && ID == 1 && !save)
+	if(click && ID == 1)
 	{
 		// set up live file to track it
 		if(mLiveFile)
diff --git a/indra/newview/llhints.cpp b/indra/newview/llhints.cpp
index e15862e2a4d..197408b40e8 100644
--- a/indra/newview/llhints.cpp
+++ b/indra/newview/llhints.cpp
@@ -171,12 +171,12 @@ LLHintPopup::LLHintPopup(const LLHintPopup::Params& p)
 	}
 	if (p.hint_image.isProvided())
 	{
-		buildFromFile("panel_hint_image.xml", NULL, p);
+		buildFromFile("panel_hint_image.xml", p);
 		getChild<LLIconCtrl>("hint_image")->setImage(p.hint_image());
 	}
 	else
 	{
-		buildFromFile( "panel_hint.xml", NULL, p);
+		buildFromFile( "panel_hint.xml", p);
 	}
 }
 
diff --git a/indra/newview/llmediactrl.cpp b/indra/newview/llmediactrl.cpp
index 7650fe92296..99b4707158f 100644
--- a/indra/newview/llmediactrl.cpp
+++ b/indra/newview/llmediactrl.cpp
@@ -564,32 +564,13 @@ void LLMediaCtrl::navigateTo( std::string url_in, std::string mime_type)
 //
 void LLMediaCtrl::navigateToLocalPage( const std::string& subdir, const std::string& filename_in )
 {
-	std::string language = LLUI::getLanguage();
-	std::string delim = gDirUtilp->getDirDelimiter();
-	std::string filename;
+	std::string filename(gDirUtilp->add(subdir, filename_in));
+	std::string expanded_filename = gDirUtilp->findSkinnedFilename("html", filename);
 
-	filename += subdir;
-	filename += delim;
-	filename += filename_in;
-
-	std::string expanded_filename = gDirUtilp->findSkinnedFilename("html", language, filename);
-
-	if (! gDirUtilp->fileExists(expanded_filename))
+	if (expanded_filename.empty())
 	{
-		if (language != "en")
-		{
-			expanded_filename = gDirUtilp->findSkinnedFilename("html", "en", filename);
-			if (! gDirUtilp->fileExists(expanded_filename))
-			{
-				llwarns << "File " << subdir << delim << filename_in << "not found" << llendl;
-				return;
-			}
-		}
-		else
-		{
-			llwarns << "File " << subdir << delim << filename_in << "not found" << llendl;
-			return;
-		}
+		llwarns << "File " << filename << "not found" << llendl;
+		return;
 	}
 	if (ensureMediaSourceExists())
 	{
@@ -597,7 +578,6 @@ void LLMediaCtrl::navigateToLocalPage( const std::string& subdir, const std::str
 		mMediaSource->setSize(mTextureWidth, mTextureHeight);
 		mMediaSource->navigateTo(expanded_filename, "text/html", false);
 	}
-
 }
 
 ////////////////////////////////////////////////////////////////////////////////
diff --git a/indra/newview/llpreviewscript.cpp b/indra/newview/llpreviewscript.cpp
index 88727bf59b4..9c25e69db00 100644
--- a/indra/newview/llpreviewscript.cpp
+++ b/indra/newview/llpreviewscript.cpp
@@ -815,7 +815,7 @@ void LLScriptEdCore::onBtnDynamicHelp()
 	if (!live_help_floater)
 	{
 		live_help_floater = new LLFloater(LLSD());
-		live_help_floater->buildFromFile("floater_lsl_guide.xml", NULL);
+		live_help_floater->buildFromFile("floater_lsl_guide.xml");
 		LLFloater* parent = dynamic_cast<LLFloater*>(getParent());
 		llassert(parent);
 		if (parent)
diff --git a/indra/newview/llsyswellwindow.cpp b/indra/newview/llsyswellwindow.cpp
index 0cb6c850122..2002647fef1 100644
--- a/indra/newview/llsyswellwindow.cpp
+++ b/indra/newview/llsyswellwindow.cpp
@@ -242,7 +242,7 @@ LLIMWellWindow::RowPanel::RowPanel(const LLSysWellWindow* parent, const LLUUID&
 		S32 chicletCounter, const std::string& name, const LLUUID& otherParticipantId) :
 		LLPanel(LLPanel::Params()), mChiclet(NULL), mParent(parent)
 {
-	buildFromFile( "panel_activeim_row.xml", NULL);
+	buildFromFile( "panel_activeim_row.xml");
 
 	// Choose which of the pre-created chiclets (IM/group) to use.
 	// The other one gets hidden.
@@ -356,7 +356,7 @@ LLIMWellWindow::ObjectRowPanel::ObjectRowPanel(const LLUUID& notification_id, bo
  : LLPanel()
  , mChiclet(NULL)
 {
-	buildFromFile( "panel_active_object_row.xml", NULL);
+	buildFromFile( "panel_active_object_row.xml");
 
 	initChiclet(notification_id);
 
diff --git a/indra/newview/lltoast.cpp b/indra/newview/lltoast.cpp
index 0eec7f0afd1..9dfb29b905c 100644
--- a/indra/newview/lltoast.cpp
+++ b/indra/newview/lltoast.cpp
@@ -118,7 +118,7 @@ LLToast::LLToast(const LLToast::Params& p)
 {
 	mTimer.reset(new LLToastLifeTimer(this, p.lifetime_secs));
 
-	buildFromFile("panel_toast.xml", NULL);
+	buildFromFile("panel_toast.xml");
 
 	setCanDrag(FALSE);
 
diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp
index 1eb4bedfaf9..47059b0b8c3 100644
--- a/indra/newview/llviewermedia.cpp
+++ b/indra/newview/llviewermedia.cpp
@@ -1184,12 +1184,9 @@ void LLViewerMedia::clearAllCookies()
 	LLDirIterator dir_iter(base_dir, "*_*");
 	while (dir_iter.next(filename))
 	{
-		target = base_dir;
-		target += filename;
-		target += gDirUtilp->getDirDelimiter();
-		target += "browser_profile";
-		target += gDirUtilp->getDirDelimiter();
-		target += "cookies";
+		target = gDirUtilp->add(base_dir, filename);
+		gDirUtilp->append(target, "browser_profile");
+		gDirUtilp->append(target, "cookies");
 		lldebugs << "target = " << target << llendl;
 		if(LLFile::isfile(target))
 		{	
@@ -1197,10 +1194,8 @@ void LLViewerMedia::clearAllCookies()
 		}
 		
 		// Other accounts may have new-style cookie files too -- delete them as well
-		target = base_dir;
-		target += filename;
-		target += gDirUtilp->getDirDelimiter();
-		target += PLUGIN_COOKIE_FILE_NAME;
+		target = gDirUtilp->add(base_dir, filename);
+		gDirUtilp->append(target, PLUGIN_COOKIE_FILE_NAME);
 		lldebugs << "target = " << target << llendl;
 		if(LLFile::isfile(target))
 		{	
diff --git a/indra/newview/llviewertexturelist.cpp b/indra/newview/llviewertexturelist.cpp
index 9a6c0569a99..7eb1a202a0f 100644
--- a/indra/newview/llviewertexturelist.cpp
+++ b/indra/newview/llviewertexturelist.cpp
@@ -1583,49 +1583,42 @@ struct UIImageDeclarations : public LLInitParam::Block<UIImageDeclarations>
 
 bool LLUIImageList::initFromFile()
 {
-	// construct path to canonical textures.xml in default skin dir
-	std::string base_file_path = gDirUtilp->getExpandedFilename(LL_PATH_SKINS, "default", "textures", "textures.xml");
+	// Look for textures.xml in all the right places. Pass merge=true because
+	// we want to overlay textures.xml from all the skins directories.
+	std::vector<std::string> textures_paths =
+		gDirUtilp->findSkinnedFilenames(LLDir::TEXTURES, "textures.xml", true);
+	std::vector<std::string>::const_iterator pi(textures_paths.begin()), pend(textures_paths.end());
+	if (pi == pend)
+	{
+		llwarns << "No textures.xml found in skins directories" << llendl;
+		return false;
+	}
 
+	// The first (most generic) file gets special validations
 	LLXMLNodePtr root;
-
-	if (!LLXMLNode::parseFile(base_file_path, root, NULL))
+	if (!LLXMLNode::parseFile(*pi, root, NULL))
 	{
-		llwarns << "Unable to parse UI image list file " << base_file_path << llendl;
+		llwarns << "Unable to parse UI image list file " << *pi << llendl;
 		return false;
 	}
 	if (!root->hasAttribute("version"))
 	{
-		llwarns << "No valid version number in UI image list file " << base_file_path << llendl;
+		llwarns << "No valid version number in UI image list file " << *pi << llendl;
 		return false;
 	}
 
 	UIImageDeclarations images;
 	LLXUIParser parser;
-	parser.readXUI(root, images, base_file_path);
-
-	// add components defined in current skin
-	std::string skin_update_path = gDirUtilp->getSkinDir() 
-									+ gDirUtilp->getDirDelimiter() 
-									+ "textures"
-									+ gDirUtilp->getDirDelimiter()
-									+ "textures.xml";
-	LLXMLNodePtr update_root;
-	if (skin_update_path != base_file_path
-		&& LLXMLNode::parseFile(skin_update_path, update_root, NULL))
-	{
-		parser.readXUI(update_root, images, skin_update_path);
-	}
-
-	// add components defined in user override of current skin
-	skin_update_path = gDirUtilp->getUserSkinDir() 
-						+ gDirUtilp->getDirDelimiter() 
-						+ "textures"
-						+ gDirUtilp->getDirDelimiter()
-						+ "textures.xml";
-	if (skin_update_path != base_file_path
-		&& LLXMLNode::parseFile(skin_update_path, update_root, NULL))
-	{
-		parser.readXUI(update_root, images, skin_update_path);
+	parser.readXUI(root, images, *pi);
+
+	// add components defined in the rest of the skin paths
+	while (++pi != pend)
+	{
+		LLXMLNodePtr update_root;
+		if (LLXMLNode::parseFile(*pi, update_root, NULL))
+		{
+			parser.readXUI(update_root, images, *pi);
+		}
 	}
 
 	if (!images.validateBlock()) return false;
diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp
index 30bb787fa7c..e569c9504f6 100644
--- a/indra/newview/llviewerwindow.cpp
+++ b/indra/newview/llviewerwindow.cpp
@@ -1685,8 +1685,7 @@ LLViewerWindow::LLViewerWindow(const Params& p)
 	LLFontGL::initClass( gSavedSettings.getF32("FontScreenDPI"),
 								mDisplayScale.mV[VX],
 								mDisplayScale.mV[VY],
-								gDirUtilp->getAppRODataDir(),
-								LLUI::getXUIPaths());
+								gDirUtilp->getAppRODataDir());
 	
 	// Create container for all sub-views
 	LLView::Params rvp;
@@ -4757,8 +4756,7 @@ void LLViewerWindow::initFonts(F32 zoom_factor)
 	LLFontGL::initClass( gSavedSettings.getF32("FontScreenDPI"),
 								mDisplayScale.mV[VX] * zoom_factor,
 								mDisplayScale.mV[VY] * zoom_factor,
-								gDirUtilp->getAppRODataDir(),
-								LLUI::getXUIPaths());
+								gDirUtilp->getAppRODataDir());
 	// Force font reloads, which can be very slow
 	LLFontGL::loadDefaultFonts();
 }
diff --git a/indra/newview/llwaterparammanager.cpp b/indra/newview/llwaterparammanager.cpp
index e3861123348..4f52ff97782 100644
--- a/indra/newview/llwaterparammanager.cpp
+++ b/indra/newview/llwaterparammanager.cpp
@@ -100,7 +100,7 @@ void LLWaterParamManager::loadPresetsFromDir(const std::string& dir)
 			break; // no more files
 		}
 
-		std::string path = dir + file;
+		std::string path = gDirUtilp->add(dir, file);
 		if (!loadPreset(path))
 		{
 			llwarns << "Error loading water preset from " << path << llendl;
diff --git a/indra/newview/llwlparammanager.cpp b/indra/newview/llwlparammanager.cpp
index 49d9d44d749..6077208799a 100644
--- a/indra/newview/llwlparammanager.cpp
+++ b/indra/newview/llwlparammanager.cpp
@@ -283,7 +283,7 @@ void LLWLParamManager::loadPresetsFromDir(const std::string& dir)
 			break; // no more files
 		}
 
-		std::string path = dir + file;
+		std::string path = gDirUtilp->add(dir, file);
 		if (!loadPreset(path))
 		{
 			llwarns << "Error loading sky preset from " << path << llendl;
diff --git a/indra/newview/skins/paths.xml b/indra/newview/skins/paths.xml
deleted file mode 100644
index 3c0da041c77..00000000000
--- a/indra/newview/skins/paths.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<paths> 
-	<directory>
-    <subdir>xui</subdir>
-    <subdir>en</subdir>
-  </directory>
-	<directory>
-    <subdir>xui</subdir>
-    <subdir>[LANGUAGE]</subdir>
-  </directory>
-</paths>
\ No newline at end of file
diff --git a/indra/newview/tests/lldir_stub.cpp b/indra/newview/tests/lldir_stub.cpp
index 18cf4e7419e..3c0a4377d80 100644
--- a/indra/newview/tests/lldir_stub.cpp
+++ b/indra/newview/tests/lldir_stub.cpp
@@ -32,7 +32,7 @@ BOOL LLDir::deleteFilesInDir(const std::string &dirname, const std::string &mask
 void LLDir::setChatLogsDir(const std::string &path) {}
 void LLDir::setPerAccountChatLogsDir(const std::string &first, const std::string &last) {}
 void LLDir::setLindenUserDir(const std::string &first, const std::string &last) {}
-void LLDir::setSkinFolder(const std::string &skin_folder) {}
+void LLDir::setSkinFolder(const std::string &skin_folder, const std::string& language) {}
 bool LLDir::setCacheDir(const std::string &path) { return true; }
 void LLDir::dumpCurrentDirectories() {}
 
diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py
index 7c6b5403e1d..e754c267336 100644
--- a/indra/newview/viewer_manifest.py
+++ b/indra/newview/viewer_manifest.py
@@ -114,7 +114,6 @@ def construct(self):
 
             # skins
             if self.prefix(src="skins"):
-                    self.path("paths.xml")
                     # include the entire textures directory recursively
                     if self.prefix(src="*/textures"):
                             self.path("*/*.tga")
diff --git a/indra/viewer_components/updater/tests/llupdaterservice_test.cpp b/indra/viewer_components/updater/tests/llupdaterservice_test.cpp
index 7c016fecf92..db52e6c55f8 100644
--- a/indra/viewer_components/updater/tests/llupdaterservice_test.cpp
+++ b/indra/viewer_components/updater/tests/llupdaterservice_test.cpp
@@ -78,7 +78,9 @@ S32 LLDir::deleteFilesInDir(const std::string &dirname,
 void LLDir::setChatLogsDir(const std::string &path){}		
 void LLDir::setPerAccountChatLogsDir(const std::string &username){}
 void LLDir::setLindenUserDir(const std::string &username){}		
-void LLDir::setSkinFolder(const std::string &skin_folder){}
+void LLDir::setSkinFolder(const std::string &skin_folder, const std::string& language){}
+std::string LLDir::getSkinFolder() const { return "default"; }
+std::string LLDir::getLanguage() const { return "en"; }
 bool LLDir::setCacheDir(const std::string &path){ return true; }
 void LLDir::dumpCurrentDirectories() {}
 
-- 
GitLab