/*==LICENSE==* CyanWorlds.com Engine - MMOG client, server and tools Copyright (C) 2011 Cyan Worlds, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Additional permissions under GNU GPL version 3 section 7 If you modify this Program, or any covered work, by linking or combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK (or a modified version of those libraries), containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the licensors of this Program grant you additional permission to convey the resulting work. Corresponding Source for a non-source form of such a combination shall include the source code for the parts of OpenSSL and IJG JPEG Library used as well as that of the covered work. You can contact Cyan Worlds, Inc. by email legal@cyan.com or by snail mail at: Cyan Worlds, Inc. 14617 N Newport Hwy Mead, WA 99021 *==LICENSE==*/ ////////////////////////////////////////////////////////////////////// // // pfLocalizationDataMgr - singleton class for managing the // localization XML data tree // ////////////////////////////////////////////////////////////////////// #include "HeadSpin.h" #include "plString.h" #include "plResMgr/plLocalization.h" #include "plFile/plEncryptedStream.h" #include "plStatusLog/plStatusLog.h" #include "pfLocalizedString.h" #include "pfLocalizationMgr.h" #include "pfLocalizationDataMgr.h" #include #include ////////////////////////////////////////////////////////////////////// // // LocalizationXMLFile - a basic class for storing all the // localization data grabbed from a single XML // file // ////////////////////////////////////////////////////////////////////// class LocalizationXMLFile { public: //replace friend by static because of conflits with subtitleXMLFile static void XMLCALL StartTag(void *userData, const XML_Char *element, const XML_Char **attributes); static void XMLCALL EndTag(void *userData, const XML_Char *element); static void XMLCALL HandleData(void *userData, const XML_Char *data, int stringLength); friend class LocalizationDatabase; // first string is language, second is data typedef std::map element; // the string is the element name typedef std::map set; // the string is the set name typedef std::map age; // the string is the age name typedef std::map ageMap; protected: bool fWeExploded; // alternative to massive error stack plFileName fFilename; XML_Parser fParser; struct tagInfo { plString fTag; std::map fAttributes; }; std::stack fTagStack; int fSkipDepth; // if we need to skip a block, this is the depth we need to skip to bool fIgnoreContents; // are we ignoring the contents between tags? plString fCurrentAge, fCurrentSet, fCurrentElement, fCurrentTranslation; ageMap fData; void IHandleLocalizationsTag(const tagInfo & parentTag, const tagInfo & thisTag); void IHandleAgeTag(const tagInfo & parentTag, const tagInfo & thisTag); void IHandleSetTag(const tagInfo & parentTag, const tagInfo & thisTag); void IHandleElementTag(const tagInfo & parentTag, const tagInfo & thisTag); void IHandleTranslationTag(const tagInfo & parentTag, const tagInfo & thisTag); public: LocalizationXMLFile() : fWeExploded(false) { } bool Parse(const plFileName & fileName); // returns false on failure void AddError(const plString & errorText); }; // A few small helper structs // I am setting these up so the header file can use this data without having to put // the LocalizationXMLFile class into the header file struct LocElementInfo { LocalizationXMLFile::element fElement; }; struct LocSetInfo { LocalizationXMLFile::set fSet; }; struct LocAgeInfo { LocalizationXMLFile::age fAge; }; ////////////////////////////////////////////////////////////////////// // Memory functions ////////////////////////////////////////////////////////////////////// static void * XMLCALL XmlMalloc (size_t size) { return malloc(size); } static void * XMLCALL XmlRealloc (void * ptr, size_t size) { return realloc(ptr, size); } static void XMLCALL XmlFree (void * ptr) { free(ptr); } XML_Memory_Handling_Suite gHeapAllocator = { XmlMalloc, XmlRealloc, XmlFree }; ////////////////////////////////////////////////////////////////////// //// XML Parsing functions /////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// void XMLCALL LocalizationXMLFile::StartTag(void *userData, const XML_Char *element, const XML_Char **attributes) { plString wElement = element; LocalizationXMLFile *file = (LocalizationXMLFile*)userData; std::map wAttributes; for (int i = 0; attributes[i]; i += 2) wAttributes[attributes[i]] = attributes[i+1]; LocalizationXMLFile::tagInfo parentTag; if (!file->fTagStack.empty()) parentTag = file->fTagStack.top(); LocalizationXMLFile::tagInfo newTag; newTag.fTag = wElement; newTag.fAttributes = wAttributes; file->fTagStack.push(newTag); if (file->fSkipDepth != -1) // we're currently skipping return; // now we handle this tag if (wElement == "localizations") file->IHandleLocalizationsTag(parentTag, newTag); else if (wElement == "age") file->IHandleAgeTag(parentTag, newTag); else if (wElement == "set") file->IHandleSetTag(parentTag, newTag); else if (wElement == "element") file->IHandleElementTag(parentTag, newTag); else if (wElement == "translation") file->IHandleTranslationTag(parentTag, newTag); else file->AddError(plFormat("Unknown tag {} found", wElement)); } void XMLCALL LocalizationXMLFile::EndTag(void *userData, const XML_Char *element) { plString wElement = element; LocalizationXMLFile *file = (LocalizationXMLFile*)userData; if (file->fSkipDepth != -1) // we're currently skipping { // check to see if we are done with the block we wanted skipped if (file->fTagStack.size() == file->fSkipDepth) file->fSkipDepth = -1; // we're done skipping } if (wElement == "age") // we left the age block file->fCurrentAge = ""; else if (wElement == "set") // we left the set block file->fCurrentSet = ""; else if (wElement == "element") // we left the element block file->fCurrentElement = ""; else if (wElement == "translation") // we left the translation block { file->fIgnoreContents = true; file->fCurrentTranslation = ""; } file->fTagStack.pop(); } void XMLCALL LocalizationXMLFile::HandleData(void *userData, const XML_Char *data, int stringLength) { LocalizationXMLFile *file = (LocalizationXMLFile*)userData; if (file->fIgnoreContents) return; // we're ignoring data, so just return if (file->fSkipDepth != -1) // we're currently skipping return; // This gets all data between tags, including indentation and newlines // so we'll have to ignore data when we aren't expecting it (not in a translation tag) plString contents = plString::FromUtf8(data, stringLength); // we must be in a translation tag since that's the only tag that doesn't ignore the contents file->fData[file->fCurrentAge][file->fCurrentSet][file->fCurrentElement][file->fCurrentTranslation] += contents; } ////////////////////////////////////////////////////////////////////// //// LocalizationXMLFile Functions /////////////////////////////////// ////////////////////////////////////////////////////////////////////// #define FILEBUFFERSIZE 8192 //// IHandleSubtitlesTag() /////////////////////////////////////////// void LocalizationXMLFile::IHandleLocalizationsTag(const LocalizationXMLFile::tagInfo & parentTag, const LocalizationXMLFile::tagInfo & thisTag) { if (!parentTag.fTag.IsEmpty()) // we only allow tags at root level { AddError("localizations tag only allowed at root level"); return; } } //// IHandleAgeTag() ///////////////////////////////////////////////// void LocalizationXMLFile::IHandleAgeTag(const LocalizationXMLFile::tagInfo & parentTag, const LocalizationXMLFile::tagInfo & thisTag) { // it has to be inside the subtitles tag if (parentTag.fTag != "localizations") { AddError("age tag can only be directly inside a localizations tag"); return; } // we have to have a name attribute if (thisTag.fAttributes.find("name") == thisTag.fAttributes.end()) { AddError("age tag is missing the name attribute"); return; } fCurrentAge = thisTag.fAttributes.find("name")->second; } //// IHandleSetTag() ///////////////////////////////////////////////// void LocalizationXMLFile::IHandleSetTag(const LocalizationXMLFile::tagInfo & parentTag, const LocalizationXMLFile::tagInfo & thisTag) { // it has to be inside the age tag if (parentTag.fTag != "age") { AddError("set tag can only be directly inside a age tag"); return; } // we have to have a name attribute if (thisTag.fAttributes.find("name") == thisTag.fAttributes.end()) { AddError("set tag is missing the name attribute"); return; } fCurrentSet = thisTag.fAttributes.find("name")->second; } //// IHandleElementTag() ///////////////////////////////////////////// void LocalizationXMLFile::IHandleElementTag(const LocalizationXMLFile::tagInfo & parentTag, const LocalizationXMLFile::tagInfo & thisTag) { // it has to be inside the element tag if (parentTag.fTag != "set") { AddError("element tag can only be directly inside a set tag"); return; } // we have to have a name attribute if (thisTag.fAttributes.find("name") == thisTag.fAttributes.end()) { AddError("element tag is missing the name attribute"); return; } fCurrentElement = thisTag.fAttributes.find("name")->second; } //// IHandleTranslationTag() ///////////////////////////////////////// void LocalizationXMLFile::IHandleTranslationTag(const LocalizationXMLFile::tagInfo & parentTag, const LocalizationXMLFile::tagInfo & thisTag) { // it has to be inside the element tag if (parentTag.fTag != "element") { AddError("translation tag can only be directly inside a element tag"); return; } // we have to have a language attribute if (thisTag.fAttributes.find("language") == thisTag.fAttributes.end()) { AddError("translation tag is missing the language attribute"); return; } fIgnoreContents = false; // we now want contents between tags fCurrentTranslation = thisTag.fAttributes.find("language")->second; } //// Parse() ///////////////////////////////////////////////////////// bool LocalizationXMLFile::Parse(const plFileName& fileName) { fFilename = fileName; while (!fTagStack.empty()) fTagStack.pop(); fCurrentAge = ""; fCurrentSet = ""; fCurrentElement = ""; fCurrentTranslation = ""; fIgnoreContents = true; fSkipDepth = -1; char Buff[FILEBUFFERSIZE]; fParser = XML_ParserCreate_MM(NULL, &gHeapAllocator, NULL); if (!fParser) { AddError("ERROR: Couldn't allocate memory for parser"); return false; } XML_SetElementHandler(fParser, StartTag, EndTag); XML_SetCharacterDataHandler(fParser, HandleData); XML_SetUserData(fParser, (void*)this); hsStream *xmlStream = plEncryptedStream::OpenEncryptedFile(fileName); if (!xmlStream) { pfLocalizationDataMgr::GetLog()->AddLineF("ERROR: Can't open file stream for %s", fileName.AsString().c_str()); return false; } bool done = false; do { size_t len; len = xmlStream->Read(FILEBUFFERSIZE, Buff); done = xmlStream->AtEnd(); if (XML_Parse(fParser, Buff, (int)len, done) == XML_STATUS_ERROR) { pfLocalizationDataMgr::GetLog()->AddLineF("ERROR: Parse error at line %d: %S", XML_GetCurrentLineNumber(fParser), XML_ErrorString(XML_GetErrorCode(fParser))); done = true; } if (fWeExploded) // some error occurred in the parser done = true; } while (!done); XML_ParserFree(fParser); fParser = nil; xmlStream->Close(); delete xmlStream; return true; } //// AddError() ////////////////////////////////////////////////////// void LocalizationXMLFile::AddError(const plString& errorText) { pfLocalizationDataMgr::GetLog()->AddLineF("ERROR (line %d): %s", XML_GetCurrentLineNumber(fParser), errorText.c_str()); fSkipDepth = fTagStack.size(); // skip this block fWeExploded = true; return; } ////////////////////////////////////////////////////////////////////// // // LocalizationDatabase - a basic class for storing all the subtitle // data grabbed from a XML directory (handles // the merging and final validation of the data) // ////////////////////////////////////////////////////////////////////// class LocalizationDatabase { protected: plFileName fDirectory; // the directory we're supposed to parse std::vector fFiles; // the various XML files in that directory LocalizationXMLFile::ageMap fData; LocalizationXMLFile::element IMergeElementData(LocalizationXMLFile::element firstElement, LocalizationXMLFile::element secondElement, const plFileName & fileName, const plString & path); LocalizationXMLFile::set IMergeSetData(LocalizationXMLFile::set firstSet, LocalizationXMLFile::set secondSet, const plFileName & fileName, const plString & path); LocalizationXMLFile::age IMergeAgeData(LocalizationXMLFile::age firstAge, LocalizationXMLFile::age secondAge, const plFileName & fileName, const plString & path); void IMergeData(); // merge all localization data in the files void IVerifyElement(const plString &ageName, const plString &setName, LocalizationXMLFile::set::iterator& curElement); void IVerifySet(const plString &ageName, const plString &setName); void IVerifyAge(const plString &ageName); void IVerifyData(); // verify the localization data once it has been merged in public: LocalizationDatabase() {} void Parse(const plFileName & directory); LocalizationXMLFile::ageMap GetData() {return fData;} }; ////////////////////////////////////////////////////////////////////// //// LocalizationDatabase Functions ////////////////////////////////// ////////////////////////////////////////////////////////////////////// //// IMergeElementData /////////////////////////////////////////////// LocalizationXMLFile::element LocalizationDatabase::IMergeElementData(LocalizationXMLFile::element firstElement, LocalizationXMLFile::element secondElement, const plFileName & fileName, const plString & path) { // copy the data over, alerting the user to any duplicate translations LocalizationXMLFile::element::iterator curTranslation; for (curTranslation = secondElement.begin(); curTranslation != secondElement.end(); curTranslation++) { if (firstElement.find(curTranslation->first) != firstElement.end()) { pfLocalizationDataMgr::GetLog()->AddLineF("Duplicate %s translation for %s found in file %s. Ignoring second translation.", curTranslation->first.c_str(), path.c_str(), fileName.AsString().c_str()); } else firstElement[curTranslation->first] = curTranslation->second; } return firstElement; } //// IMergeSetData /////////////////////////////////////////////////// LocalizationXMLFile::set LocalizationDatabase::IMergeSetData(LocalizationXMLFile::set firstSet, LocalizationXMLFile::set secondSet, const plFileName & fileName, const plString & path) { // Merge all the elements LocalizationXMLFile::set::iterator curElement; for (curElement = secondSet.begin(); curElement != secondSet.end(); curElement++) { // if the element doesn't exist in the current set, add it if (firstSet.find(curElement->first) == firstSet.end()) firstSet[curElement->first] = curElement->second; else // merge the element in firstSet[curElement->first] = IMergeElementData(firstSet[curElement->first], curElement->second, fileName, plFormat("{}.{}", path, curElement->first)); } return firstSet; } //// IMergeAgeData /////////////////////////////////////////////////// LocalizationXMLFile::age LocalizationDatabase::IMergeAgeData(LocalizationXMLFile::age firstAge, LocalizationXMLFile::age secondAge, const plFileName & fileName, const plString & path) { // Merge all the sets LocalizationXMLFile::age::iterator curSet; for (curSet = secondAge.begin(); curSet != secondAge.end(); curSet++) { // if the set doesn't exist in the current age, just add it if (firstAge.find(curSet->first) == firstAge.end()) firstAge[curSet->first] = curSet->second; else // merge the data in firstAge[curSet->first] = IMergeSetData(firstAge[curSet->first], curSet->second, fileName, plFormat("{}.{}", path, curSet->first)); } return firstAge; } //// IMergeData() //////////////////////////////////////////////////// void LocalizationDatabase::IMergeData() { for (int i = 0; i < fFiles.size(); i++) { LocalizationXMLFile::ageMap fileData = fFiles[i].fData; LocalizationXMLFile::ageMap::iterator curAge; for (curAge = fileData.begin(); curAge != fileData.end(); curAge++) { // if the age doesn't exist in the current merged database, just add it with no more checking if (fData.find(curAge->first) == fData.end()) fData[curAge->first] = curAge->second; else // otherwise, merge the data in fData[curAge->first] = IMergeAgeData(fData[curAge->first], curAge->second, fFiles[i].fFilename, curAge->first); } } } //// IVerifyElement() //////////////////////////////////////////////// void LocalizationDatabase::IVerifyElement(const plString &ageName, const plString &setName, LocalizationXMLFile::set::iterator& curElement) { std::vector languageNames; plString defaultLanguage; int numLocales = plLocalization::GetNumLocales(); for (int curLocale = 0; curLocale <= numLocales; curLocale++) { plString name = plLocalization::GetLanguageName((plLocalization::Language)curLocale); languageNames.push_back(name); } defaultLanguage = languageNames[0]; plString elementName = curElement->first; LocalizationXMLFile::element& theElement = curElement->second; LocalizationXMLFile::element::iterator curTranslation = theElement.begin(); while (curTranslation != theElement.end()) { // Make sure this language exists! bool languageExists = false; for (int i = 0; i < languageNames.size(); i++) { if (languageNames[i] == curTranslation->first) { languageExists = true; break; } } if (!languageExists) { pfLocalizationDataMgr::GetLog()->AddLineF("ERROR: The language %s used by %s.%s.%s is not supported. Discarding translation.", curTranslation->first.c_str(), ageName.c_str(), setName.c_str(), elementName.c_str()); curTranslation = theElement.erase(curTranslation); } else curTranslation++; } for (int i = 1; i < languageNames.size(); i++) { if (theElement.find(languageNames[i]) == theElement.end()) { pfLocalizationDataMgr::GetLog()->AddLineF("WARNING: Language %s is missing from the translations in element %s.%s.%s. You'll want to get translations for that!", languageNames[i].c_str(), ageName.c_str(), setName.c_str(), elementName.c_str()); } } } //// IVerifySet() //////////////////////////////////////////////////// void LocalizationDatabase::IVerifySet(const plString &ageName, const plString &setName) { LocalizationXMLFile::set& theSet = fData[ageName][setName]; LocalizationXMLFile::set::iterator curElement = theSet.begin(); plString defaultLanguage = plLocalization::GetLanguageName((plLocalization::Language)0); while (curElement != theSet.end()) { // Check that we at least have a default language translation for fallback if (curElement->second.find(defaultLanguage) == curElement->second.end()) { pfLocalizationDataMgr::GetLog()->AddLineF("ERROR: Default language %s is missing from the translations in element %s.%s.%s. Deleting element.", defaultLanguage.c_str(), ageName.c_str(), setName.c_str(), curElement->first.c_str()); curElement = theSet.erase(curElement); } else { IVerifyElement(ageName, setName, curElement); curElement++; } } } //// IVerifyAge() //////////////////////////////////////////////////// void LocalizationDatabase::IVerifyAge(const plString &ageName) { LocalizationXMLFile::age& theAge = fData[ageName]; LocalizationXMLFile::age::iterator curSet; for (curSet = theAge.begin(); curSet != theAge.end(); curSet++) IVerifySet(ageName, curSet->first); } //// IVerifyData() /////////////////////////////////////////////////// void LocalizationDatabase::IVerifyData() { LocalizationXMLFile::ageMap::iterator curAge; for (curAge = fData.begin(); curAge != fData.end(); curAge++) IVerifyAge(curAge->first); } //// Parse() ///////////////////////////////////////////////////////// void LocalizationDatabase::Parse(const plFileName & directory) { fDirectory = directory; fFiles.clear(); std::vector locFiles = plFileSystem::ListDir(directory, "*.loc"); for (auto iter = locFiles.begin(); iter != locFiles.end(); ++iter) { LocalizationXMLFile newFile; bool retVal = newFile.Parse(*iter); if (!retVal) pfLocalizationDataMgr::GetLog()->AddLineF("WARNING: Errors in file %s", iter->GetFileName().c_str()); fFiles.push_back(newFile); pfLocalizationDataMgr::GetLog()->AddLineF("File %s parsed and added to database", iter->GetFileName().c_str()); } IMergeData(); IVerifyData(); return; } ////////////////////////////////////////////////////////////////////// //// pf3PartMap Functions //////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// //// ISplitString() ////////////////////////////////////////////////// template void pfLocalizationDataMgr::pf3PartMap::ISplitString(plString key, plString &age, plString &set, plString &name) { std::vector tokens = key.Tokenize("."); if (tokens.size() >= 1) age = tokens[0]; if (tokens.size() >= 2) set = tokens[1]; if (tokens.size() >= 3) name = tokens[2]; } //// exists() //////////////////////////////////////////////////////// template bool pfLocalizationDataMgr::pf3PartMap::exists(const plString & key) { plString age, set, name; ISplitString(key, age, set, name); if (age.IsEmpty() || set.IsEmpty() || name.IsEmpty()) // if any are missing, it's invalid, so we don't have it return false; // now check individually auto curAge = fData.find(age); if (curAge == fData.end()) // age doesn't exist return false; auto curSet = curAge->second.find(set); if (curSet == curAge->second.end()) // set doesn't exist return false; auto curElement = curSet->second.find(name); if (curElement == curSet->second.end()) // name doesn't exist return false; // we passed all the tests, return true! return true; } //// setExists() ///////////////////////////////////////////////////// template bool pfLocalizationDataMgr::pf3PartMap::setExists(const plString & key) { plString age, set, name; ISplitString(key, age, set, name); if (age.IsEmpty() || set.IsEmpty()) // if any are missing, it's invalid, so we don't have it (ignoring name) return false; // now check individually auto curAge = fData.find(age); if (curAge == fData.end()) // age doesn't exist return false; auto curSet = curAge->second.find(set); if (curSet == curAge->second.end()) // set doesn't exist return false; // we passed all the tests, return true! return true; } //// erase() ///////////////////////////////////////////////////////// template void pfLocalizationDataMgr::pf3PartMap::erase(const plString & key) { plString age, set, name; ISplitString(key, age, set, name); if (age.IsEmpty() || set.IsEmpty() || name.IsEmpty()) // if any are missing, it's invalid, so we don't delete it return; // now check individually auto curAge = fData.find(age); if (curAge == fData.end()) // age doesn't exist return; auto curSet = curAge->second.find(set); if (curSet == curAge->second.end()) // set doesn't exist return; auto curElement = curSet->second.find(name); if (curElement == curSet->second.end()) // name doesn't exist return; // ok, so now we want to nuke it! curSet->second.erase(name); if (curSet->second.size() == 0) // is the set now empty? curAge->second.erase(curSet); // nuke it! if (curAge->second.size() == 0) // is the age now empty? fData.erase(curAge); // nuke it! } //// operator[]() //////////////////////////////////////////////////// template mapT &pfLocalizationDataMgr::pf3PartMap::operator[](const plString &key) { plString age, set, name; ISplitString(key, age, set, name); return fData[age][set][name]; } //// getAgeList() //////////////////////////////////////////////////// template std::vector pfLocalizationDataMgr::pf3PartMap::getAgeList() { std::vector retVal; typename ThreePartMap::iterator curAge; for (curAge = fData.begin(); curAge != fData.end(); curAge++) retVal.push_back(curAge->first); return retVal; } //// getSetList() //////////////////////////////////////////////////// template std::vector pfLocalizationDataMgr::pf3PartMap::getSetList(const plString & age) { std::vector retVal; typename std::map >::iterator curSet; auto curAge = fData.find(age); if (curAge == fData.end()) return retVal; // return an empty list, the age doesn't exist for (curSet = curAge->second.begin(); curSet != curAge->second.end(); curSet++) retVal.push_back(curSet->first); return retVal; } //// getNameList() /////////////////////////////////////////////////// template std::vector pfLocalizationDataMgr::pf3PartMap::getNameList(const plString & age, const plString & set) { std::vector retVal; typename std::map::iterator curName; auto curAge = fData.find(age); if (curAge == fData.end()) return retVal; // return an empty list, the age doesn't exist auto curSet = curAge->second.find(set); if (curSet == curAge->second.end()) return retVal; // return an empty list, the set doesn't exist for (curName = curSet->second.begin(); curName != curSet->second.end(); curName++) retVal.push_back(curName->first); return retVal; } ////////////////////////////////////////////////////////////////////// //// pfLocalizationDataMgr Functions ///////////////////////////////// ////////////////////////////////////////////////////////////////////// pfLocalizationDataMgr *pfLocalizationDataMgr::fInstance = nil; plStatusLog *pfLocalizationDataMgr::fLog = nil; // output logfile //// Constructor/Destructor ////////////////////////////////////////// pfLocalizationDataMgr::pfLocalizationDataMgr(const plFileName & path) { hsAssert(!fInstance, "Tried to create the localization data manager more than once!"); fInstance = this; fDataPath = path; fDatabase = nil; } pfLocalizationDataMgr::~pfLocalizationDataMgr() { fInstance = nil; if (fDatabase) { delete fDatabase; fDatabase = nil; } } //// ICreateLocalizedElement ///////////////////////////////////////// pfLocalizationDataMgr::localizedElement pfLocalizationDataMgr::ICreateLocalizedElement() { int numLocales = plLocalization::GetNumLocales(); pfLocalizationDataMgr::localizedElement retVal; for (int curLocale = 0; curLocale <= numLocales; curLocale++) { retVal[plLocalization::GetLanguageName((plLocalization::Language)curLocale)] = ""; } return retVal; } //// IGetCurrentLanguageName ///////////////////////////////////////// plString pfLocalizationDataMgr::IGetCurrentLanguageName() { return plLocalization::GetLanguageName(plLocalization::GetLanguage()); } //// IGetAllLanguageNames //////////////////////////////////////////// std::vector pfLocalizationDataMgr::IGetAllLanguageNames() { int numLocales = plLocalization::GetNumLocales(); std::vector retVal; for (int curLocale = 0; curLocale <= numLocales; curLocale++) { plString name = plLocalization::GetLanguageName((plLocalization::Language)curLocale); retVal.push_back(name); } return retVal; } //// IConvertSubtitle //////////////////////////////////////////////// void pfLocalizationDataMgr::IConvertElement(LocElementInfo *elementInfo, const plString & curPath) { pfLocalizationDataMgr::localizedElement newElement; int16_t numArgs = -1; LocalizationXMLFile::element::iterator curTranslation; for (curTranslation = elementInfo->fElement.begin(); curTranslation != elementInfo->fElement.end(); curTranslation++) { newElement[curTranslation->first].FromXML(curTranslation->second); uint16_t argCount = newElement[curTranslation->first].GetArgumentCount(); if (numArgs == -1) // just started numArgs = argCount; else if (argCount != numArgs) fLog->AddLineF("WARNING: Argument number mismatch in element %s for %s", curPath.c_str(), curTranslation->first.c_str()); } fLocalizedElements[curPath] = newElement; } //// IConvertSet ///////////////////////////////////////////////////// void pfLocalizationDataMgr::IConvertSet(LocSetInfo *setInfo, const plString & curPath) { LocalizationXMLFile::set::iterator curElement; for (curElement = setInfo->fSet.begin(); curElement != setInfo->fSet.end(); curElement++) { LocElementInfo elementInfo; elementInfo.fElement = curElement->second; IConvertElement(&elementInfo, plFormat("{}.{}", curPath, curElement->first)); } } //// IConvertAge ///////////////////////////////////////////////////// void pfLocalizationDataMgr::IConvertAge(LocAgeInfo *ageInfo, const plString & curPath) { LocalizationXMLFile::age::iterator curSet; for (curSet = ageInfo->fAge.begin(); curSet != ageInfo->fAge.end(); curSet++) { LocSetInfo setInfo; setInfo.fSet = curSet->second; IConvertSet(&setInfo, plFormat("{}.{}", curPath, curSet->first)); } } //// IWriteText ////////////////////////////////////////////////////// void pfLocalizationDataMgr::IWriteText(const plFileName & filename, const plString & ageName, const plString & languageName) { bool weWroteData = false; // did we actually write any data of consequence? bool setEmpty = true; // we will try to pretty print it all so it's easy to read for the devs plStringStream fileData; fileData << "\n"; fileData << "\n"; fileData << plFormat("\t\n", ageName); std::vector setNames = GetSetList(ageName); for (int curSet = 0; curSet < setNames.size(); curSet++) { setEmpty = true; // so far, this set is empty plStringStream setCode; setCode << plFormat("\t\t\n", setNames[curSet]); std::vector elementNames = GetElementList(ageName, setNames[curSet]); for (int curElement = 0; curElement < elementNames.size(); curElement++) { setCode << plFormat("\t\t\t\n", elementNames[curElement]); plString key = plFormat("{}.{}.{}", ageName, setNames[curSet], elementNames[curElement]); if (fLocalizedElements[key].find(languageName) != fLocalizedElements[key].end()) { weWroteData = true; setEmpty = false; setCode << plFormat("\t\t\t\t", languageName); setCode << fLocalizedElements[key][languageName].ToXML(); setCode << "\n"; } setCode << "\t\t\t\n"; } setCode << "\t\t\n"; if (!setEmpty) fileData << setCode.GetString(); } fileData << "\t\n"; fileData << "\n"; if (weWroteData) { // now spit the results out to the file hsStream *xmlStream = plEncryptedStream::OpenEncryptedFileWrite(filename); xmlStream->Write(fileData.GetLength(), fileData.GetRawBuffer()); xmlStream->Close(); delete xmlStream; } } //// Initialize ////////////////////////////////////////////////////// void pfLocalizationDataMgr::Initialize(const plFileName & path) { if (fInstance) return; fInstance = new pfLocalizationDataMgr(path); fLog = plStatusLogMgr::GetInstance().CreateStatusLog(30, "LocalizationDataMgr.log", plStatusLog::kFilledBackground | plStatusLog::kAlignToTop | plStatusLog::kTimestamp); fInstance->SetupData(); } //// Shutdown //////////////////////////////////////////////////////// void pfLocalizationDataMgr::Shutdown() { if ( fLog != nil ) { delete fLog; fLog = nil; } if (fInstance) { delete fInstance; fInstance = nil; } } //// SetupData /////////////////////////////////////////////////////// void pfLocalizationDataMgr::SetupData() { if (fDatabase) delete fDatabase; fDatabase = new LocalizationDatabase(); fDatabase->Parse(fDataPath); fLog->AddLine("File reading complete, converting to native data format"); // and now we read all the data out of the database and convert it to our native formats // transfer localization data LocalizationXMLFile::ageMap data = fDatabase->GetData(); LocalizationXMLFile::ageMap::iterator curAge; for (curAge = data.begin(); curAge != data.end(); curAge++) { LocAgeInfo ageInfo; ageInfo.fAge = curAge->second; IConvertAge(&ageInfo, curAge->first); } OutputTreeToLog(); } //// GetElement ////////////////////////////////////////////////////// pfLocalizedString pfLocalizationDataMgr::GetElement(const plString & name) { pfLocalizedString retVal; // if this returns before we initialize it, it will be empty, indicating failure if (!fLocalizedElements.exists(name)) // does the requested element exist? return retVal; // nope, so return failure plString languageName = IGetCurrentLanguageName(); if (fLocalizedElements[name].find(languageName) == fLocalizedElements[name].end()) // current language isn't specified { languageName = "English"; // force to english if (fLocalizedElements[name].find(languageName) == fLocalizedElements[name].end()) // make sure english exists return retVal; // language doesn't exist } retVal = fLocalizedElements[name][languageName]; return retVal; } //// GetSpecificElement ////////////////////////////////////////////// pfLocalizedString pfLocalizationDataMgr::GetSpecificElement(const plString & name, const plString & language) { pfLocalizedString retVal; // if this returns before we initialize it, it will have an ID of 0, indicating failure if (!fLocalizedElements.exists(name)) // does the requested subtitle exist? return retVal; // nope, so return failure if (fLocalizedElements[name].find(language) == fLocalizedElements[name].end()) return retVal; // language doesn't exist retVal = fLocalizedElements[name][language]; return retVal; } //// GetLanguages //////////////////////////////////////////////////// std::vector pfLocalizationDataMgr::GetLanguages(const plString & ageName, const plString & setName, const plString & elementName) { std::vector retVal; plString key = plFormat("{}.{}.{}", ageName, setName, elementName); if (fLocalizedElements.exists(key)) { // age, set, and element exists localizedElement elem = fLocalizedElements[key]; localizedElement::iterator curLanguage; for (curLanguage = elem.begin(); curLanguage != elem.end(); curLanguage++) { plString language = curLanguage->first; if (!language.IsEmpty()) // somehow blank language names sneak in... so don't return them retVal.push_back(curLanguage->first); } } return retVal; } //// GetElementXMLData /////////////////////////////////////////////// plString pfLocalizationDataMgr::GetElementXMLData(const plString & name, const plString & languageName) { if (fLocalizedElements.exists(name) && (fLocalizedElements[name].find(languageName) != fLocalizedElements[name].end())) return fLocalizedElements[name][languageName].ToXML(); return ""; } //// GetElementPlainTextData ///////////////////////////////////////// plString pfLocalizationDataMgr::GetElementPlainTextData(const plString & name, const plString & languageName) { if (fLocalizedElements.exists(name) && (fLocalizedElements[name].find(languageName) != fLocalizedElements[name].end())) return fLocalizedElements[name][languageName]; return ""; } //// SetElementXMLData /////////////////////////////////////////////// bool pfLocalizationDataMgr::SetElementXMLData(const plString & name, const plString & languageName, const plString & xmlData) { if (!fLocalizedElements.exists(name)) return false; // doesn't exist fLocalizedElements[name][languageName].FromXML(xmlData); return true; } //// SetElementPlainTextData ///////////////////////////////////////// bool pfLocalizationDataMgr::SetElementPlainTextData(const plString & name, const plString & languageName, const plString & plainText) { if (!fLocalizedElements.exists(name)) return false; // doesn't exist fLocalizedElements[name][languageName] = plainText; return true; } //// AddLocalization ///////////////////////////////////////////////// bool pfLocalizationDataMgr::AddLocalization(const plString & name, const plString & newLanguage) { if (!fLocalizedElements.exists(name)) return false; // doesn't exist // copy the english over so it can be localized fLocalizedElements[name][newLanguage] = fLocalizedElements[name]["English"]; return true; } //// AddElement ////////////////////////////////////////////////////// bool pfLocalizationDataMgr::AddElement(const plString & name) { if (fLocalizedElements.exists(name)) return false; // already exists pfLocalizedString newElement; fLocalizedElements[name]["English"] = newElement; return true; } //// DeleteLocalization ////////////////////////////////////////////// bool pfLocalizationDataMgr::DeleteLocalization(const plString & name, const plString & languageName) { if (!fLocalizedElements.exists(name)) return false; // doesn't exist if (fLocalizedElements[name].find(languageName) == fLocalizedElements[name].end()) return false; // doesn't exist fLocalizedElements[name].erase(languageName); return true; } //// DeleteElement /////////////////////////////////////////////////// bool pfLocalizationDataMgr::DeleteElement(const plString & name) { if (!fLocalizedElements.exists(name)) return false; // doesn't exist // delete it! fLocalizedElements.erase(name); return true; } //// WriteDatabaseToDisk ///////////////////////////////////////////// void pfLocalizationDataMgr::WriteDatabaseToDisk(const plFileName & path) { std::vector ageNames = GetAgeList(); std::vector languageNames = IGetAllLanguageNames(); for (int curAge = 0; curAge < ageNames.size(); curAge++) { for (int curLanguage = 0; curLanguage < languageNames.size(); curLanguage++) { plFileName locPath = plFileName::Join(path, plFormat("{}{}.loc", ageNames[curAge], languageNames[curLanguage])); IWriteText(locPath, ageNames[curAge], languageNames[curLanguage]); } } } //// OutputTreeToLog ///////////////////////////////////////////////// void pfLocalizationDataMgr::OutputTreeToLog() { std::vector ages = GetAgeList(); fLog->AddLine("\n"); fLog->AddLine("Localization tree:\n"); for (std::vector::iterator i = ages.begin(); i != ages.end(); ++i) { plString age = *i; fLog->AddLineF("\t%s", age.c_str()); std::vector sets = GetSetList(age); for (std::vector::iterator j = sets.begin(); j != sets.end(); ++j) { plString set = (*j); fLog->AddLineF("\t\t%s", set.c_str()); std::vector names = GetElementList(age, set); for (std::vector::iterator k = names.begin(); k != names.end(); ++k) { plString name = (*k); fLog->AddLineF("\t\t\t%s", name.c_str()); } } } }