Browse Source

Merge remote-tracking branch '1vanK/improvements'

Lasse Öörni 10 years ago
parent
commit
87c8a0874a

+ 36 - 0
Docs/AngelScriptAPI.h

@@ -5668,6 +5668,40 @@ int weakRefs;
 int width;
 };
 
+class Localization
+{
+// Methods:
+String Get(const String&);
+String GetLanguage(int);
+int GetLanguageIndex(const String&);
+void LoadJSON(const JSONValue&);
+void LoadJSONFile(const String&);
+void Reset();
+void SendEvent(const String&, VariantMap& = VariantMap ( ));
+void SetLanguage(const String&);
+void SetLanguage(int);
+
+// Properties:
+/* readonly */
+StringHash baseType;
+/* readonly */
+String category;
+/* readonly */
+String language;
+/* readonly */
+int languageIndex;
+/* readonly */
+int numLanguages;
+/* readonly */
+int refs;
+/* readonly */
+StringHash type;
+/* readonly */
+String typeName;
+/* readonly */
+int weakRefs;
+};
+
 class Log
 {
 // Methods:
@@ -10416,6 +10450,7 @@ Array<Variant> attributeDefaults;
 /* readonly */
 Array<AttributeInfo> attributeInfos;
 Array<Variant> attributes;
+bool autoLocalizable;
 /* readonly */
 StringHash baseType;
 bool bringToBack;
@@ -13171,6 +13206,7 @@ Engine engine;
 FileSystem fileSystem;
 Graphics graphics;
 Input input;
+Localization localization;
 Log log;
 Network network;
 Node node;

+ 34 - 5
Docs/LuaScriptAPI.dox

@@ -98,6 +98,7 @@ namespace Urho3D
 <a href="#Class_Light"><b>Light</b></a>
 <a href="#Class_LineEdit"><b>LineEdit</b></a>
 <a href="#Class_ListView"><b>ListView</b></a>
+<a href="#Class_Localization"><b>Localization</b></a>
 <a href="#Class_Log"><b>Log</b></a>
 <a href="#Class_LuaScriptInstance"><b>LuaScriptInstance</b></a>
 <a href="#Class_Material"><b>Material</b></a>
@@ -2983,6 +2984,29 @@ Properties:
 - bool hierarchyMode
 - int baseIndent
 
+<a name="Class_Localization"></a>
+### Localization : Object
+
+Methods:
+
+- int GetNumLanguages() const
+- int GetLanguageIndex() const
+- int GetLanguageIndex(const String language)
+- String GetLanguage()
+- String GetLanguage(int index)
+- void SetLanguage(int index)
+- void SetLanguage(const String language)
+- String Get(const String id)
+- void Reset()
+- void LoadJSON(const JSONValue& source)
+- void LoadJSONFile(const String name)
+
+Properties:
+
+- int numLanguages (readonly)
+- int languageIndex (readonly)
+- String language (readonly)
+
 <a name="Class_Log"></a>
 ### Log : Object
 
@@ -5707,6 +5731,8 @@ Methods:
 - void SetHoverColor(const Color& color)
 - void SetTextEffect(TextEffect textEffect)
 - void SetEffectColor(const Color& effectColor)
+- bool GetAutoLocalizable() const
+- void SetAutoLocalizable(bool enable)
 - Font* GetFont() const
 - int GetFontSize() const
 - const String GetText() const
@@ -5736,6 +5762,7 @@ Properties:
 - HorizontalAlignment textAlignment
 - float rowSpacing
 - bool wordwrap
+- bool autoLocalizable
 - unsigned selectionStart (readonly)
 - unsigned selectionLength (readonly)
 - Color& selectionColor
@@ -7632,6 +7659,7 @@ Properties:
 - Graphics* GetGraphics()
 - Input* GetInput()
 - String GetInternalPath(const String pathName)
+- Localization* GetLocalization()
 - Log* GetLog()
 - String GetNativePath(const String pathName)
 - Network* GetNetwork()
@@ -7659,11 +7687,11 @@ Properties:
 - void PrintLine(const String str, bool error = false)
 - int Rand()
 - float RandStandardNormal()
-- float Random()
-- float Random(float min, float max)
 - float Random(float range)
-- int RandomInt(int min, int max)
+- float Random(float min, float max)
+- float Random()
 - int RandomInt(int range)
+- int RandomInt(int min, int max)
 - float RandomNormal(float meanValue, float variance)
 - String RemoveTrailingSlash(const String pathName)
 - String ReplaceExtension(const String fullPath, const String newExtension)
@@ -7674,8 +7702,8 @@ Properties:
 - float Sign(float value)
 - float Sin(float angle)
 - float SmoothStep(float lhs, float rhs, float t)
-- void SubscribeToEvent(void* sender, const String eventName, void* functionOrFunctionName)
 - void SubscribeToEvent(const String eventName, void* functionOrFunctionName)
+- void SubscribeToEvent(void* sender, const String eventName, void* functionOrFunctionName)
 - float Tan(float angle)
 - bool ToBool(const String source)
 - Color ToColor(const String source)
@@ -7698,8 +7726,8 @@ Properties:
 - Vector4 ToVector4(const String source, bool allowMissingCoords = false)
 - void UnsubscribeFromAllEvents()
 - void UnsubscribeFromAllEventsExcept(const Vector<String>& exceptionNames)
-- void UnsubscribeFromEvent(Object* sender, const String eventName)
 - void UnsubscribeFromEvent(const String eventName)
+- void UnsubscribeFromEvent(Object* sender, const String eventName)
 - void UnsubscribeFromEvents(Object* sender)
 
 
@@ -7712,6 +7740,7 @@ Properties:
 - FileSystem* fileSystem (readonly)
 - Graphics* graphics (readonly)
 - Input* input (readonly)
+- Localization* localization (readonly)
 - Log* log (readonly)
 - Network* network (readonly)
 - Renderer* renderer (readonly)

+ 87 - 0
Docs/Reference.dox

@@ -392,6 +392,93 @@ When writing new resource types, the background loading mechanism requires imple
 
 If a resource depends on other resources, writing efficient threaded loading for it can be hard, as calling GetResource() is not allowed inside BeginLoad() when background loading. There are a few options: it is allowed to queue new background load requests by calling BackgroundLoadResource() within BeginLoad(), or if the needed resource does not need to be permanently stored in the cache and is safe to load outside the main thread (for example Image or XMLFile, which do not possess any GPU-side data), \ref ResourceCache::GetTempResource "GetTempResource()" can be called inside BeginLoad.
 
+\page Localization Localization
+
+The Localization subsystem provides a simple way to creating multilingual applications.
+
+\section LocalizationInit Initialization
+
+Before using the subsystem must load a collection of strings. A common practice is to make it into function Start(). Allowed loading of multiple files.
+
+\code
+Localization* l10n = GetSubsystem<Localization>();
+l10n->LoadJSONFile("StringsEnRu.json");
+l10n->LoadJSONFile("StringsDe.json");
+\endcode
+
+JSON files must be in UTF8 encoding without BOM. Sample files are in a directory Data. The JSON files have the following format:
+
+\code
+{
+	"string id 1":{
+		"language 1":"value11",
+		"language 2":"value12",
+		"language 3":"value13"
+	},
+	"string id 2":{
+		"language 1":"value21",
+		"language 2":"value22",
+		"language 3":"value23"
+	}
+}
+\endcode
+
+It can be defined any number of languages. Remember that language names and string identifiers are case sensitive. "En" and "en" are considered different languages.
+During the loading process languages are numbered in order of finding. Indexing starts from zero. The first founded language immediately appointed as the current language.
+
+When initialization is usually also need to hook a function to be called when you change the language.
+
+\code
+SubscribeToEvent(E_CHANGELANGUAGE, HANDLER(ClassName, HandleChangeLanguage));
+\endcode
+
+\section LocalizationUsing Using
+
+The Get function returns a string with the specified string identifier in the current language.
+
+\code
+Text* t = new Text(context_);
+t->SetName("Text1");
+Localization* l10n = GetSubsystem<Localization>();
+t->SetText(l10n->Get("string 1"));
+\endcode
+
+This function returns String::EMPTY if the string id is empty. This function returns the id if translation is not found and will be added a warning into the log.
+
+It is also necessary to implement a reaction to change the language.
+
+\code
+void Game::HandleChangeLanguage(StringHash eventType, VariantMap& eventData)
+{
+    Localization* l10n = GetSubsystem<Localization>();
+    ...
+    Text* t = static_cast<Text*>(uiRoot->GetChild("Text1", true));
+    t->SetText(l10n->Get("string 1"));
+}
+\endcode
+
+Like that You can change texts, sprites and other aspects of the game when language is changed.
+
+For Text elements is possible automatic translations.
+
+\code
+Text* t2 = new Text(context_);
+t2->SetText("string 2");
+t2->SetAutoLocalizable(true);
+\endcode
+
+Wherein text value is used as an identifier. In this case, the change of text in HandleChangeLanguage is not necessary.
+
+Change the language allows function SetLanguage().
+
+\code
+Localization* l10n = GetSubsystem<Localization>();
+l10n->SetLanguage("language 2");
+l10n->SetLanguage(0);
+\endcode
+
+Also see the example 40_Localization.
+
 \page Scripting Scripting
 
 To enable AngelScript scripting support, the Script subsystem needs to be created and registered after initializing the Engine. This is accomplished by the following code, seen eg. in Tools/Urho3DPlayer/Urho3DPlayer.cpp:

+ 34 - 0
Docs/ScriptAPI.dox

@@ -413,6 +413,8 @@ namespace Urho3D
 - %Success : bool
 - %Resource : Resource pointer
 
+### ChangeLanguage
+
 ## %Scene events
 
 ### SceneUpdate
@@ -1925,6 +1927,7 @@ namespace Urho3D
 - %Text %Alignment : int
 - %Row %Spacing : float
 - %Word %Wrap : bool
+- %Auto %Localizable : bool
 - %Selection %Color : Color
 - %Hover %Color : Color
 - %Text %Effect : int
@@ -2242,6 +2245,7 @@ namespace Urho3D
 <a href="#Class_Light"><b>Light</b></a>
 <a href="#Class_LineEdit"><b>LineEdit</b></a>
 <a href="#Class_ListView"><b>ListView</b></a>
+<a href="#Class_Localization"><b>Localization</b></a>
 <a href="#Class_Log"><b>Log</b></a>
 <a href="#Class_Material"><b>Material</b></a>
 <a href="#Class_Matrix3"><b>Matrix3</b></a>
@@ -7216,6 +7220,34 @@ Properties:
 - int weakRefs // readonly
 - int width
 
+<a name="Class_Localization"></a>
+
+### Localization
+
+Methods:
+
+- String Get(const String&)
+- String GetLanguage(int)
+- int GetLanguageIndex(const String&)
+- void LoadJSON(const JSONValue&)
+- void LoadJSONFile(const String&)
+- void Reset()
+- void SendEvent(const String&, VariantMap& = VariantMap ( ))
+- void SetLanguage(const String&)
+- void SetLanguage(int)
+
+Properties:
+
+- StringHash baseType // readonly
+- String category // readonly
+- String language // readonly
+- int languageIndex // readonly
+- int numLanguages // readonly
+- int refs // readonly
+- StringHash type // readonly
+- String typeName // readonly
+- int weakRefs // readonly
+
 <a name="Class_Log"></a>
 
 ### Log
@@ -11398,6 +11430,7 @@ Properties:
 - Variant[] attributeDefaults // readonly
 - AttributeInfo[] attributeInfos // readonly
 - Variant[] attributes
+- bool autoLocalizable
 - StringHash baseType // readonly
 - bool bringToBack
 - bool bringToFront
@@ -13834,6 +13867,7 @@ Properties:
 - FileSystem@ fileSystem
 - Graphics@ graphics
 - Input@ input
+- Localization@ localization
 - Log@ log
 - Network@ network
 - Node@ node

+ 1 - 0
Docs/Urho3D.dox

@@ -25,6 +25,7 @@ For further reference, see:
 \ref MainLoop "Engine initialization and main loop" <br>
 \ref SceneModel "Scene model" <br>
 \ref Resources "Resources" <br>
+\ref Localization "Localization" <br>
 \ref Scripting "Scripting" <br>
 \ref LuaScripting "Lua scripting" <br>
 \ref Rendering "Rendering" <br>

+ 33 - 0
Source/Samples/40_Localization/CMakeLists.txt

@@ -0,0 +1,33 @@
+#
+# Copyright (c) 2008-2015 the Urho3D project.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+# Define target name
+set (TARGET_NAME 40_Localization)
+
+# Define source files
+define_source_files (EXTRA_H_FILES ${COMMON_SAMPLE_H_FILES})
+
+# Setup target with resource copying
+setup_main_executable ()
+
+# Setup test cases
+setup_test ()

+ 239 - 0
Source/Samples/40_Localization/L10n.cpp

@@ -0,0 +1,239 @@
+//
+// Copyright (c) 2008-2015 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include <Urho3D/Urho3D.h>
+
+#include <Urho3D/Resource/Localization.h>
+#include <Urho3D/Core/CoreEvents.h>
+#include <Urho3D/UI/Window.h>
+#include <Urho3D/UI/Text.h>
+#include <Urho3D/UI/Button.h>
+#include <Urho3D/UI/Font.h>
+#include <Urho3D/Graphics/StaticModel.h>
+#include <Urho3D/Graphics/Material.h>
+#include <Urho3D/Graphics/Model.h>
+#include <Urho3D/Graphics/Octree.h>
+#include <Urho3D/UI/Text3D.h>
+#include <Urho3D/UI/UIEvents.h>
+#include <Urho3D/Graphics/Zone.h>
+#include <Urho3D/Resource/ResourceEvents.h>
+
+#include "L10n.h"
+
+#include <Urho3D/DebugNew.h>
+
+DEFINE_APPLICATION_MAIN(L10n)
+
+L10n::L10n(Context* context) :
+    Sample(context)
+{
+}
+
+void L10n::Start()
+{
+    // Execute base class startup
+    Sample::Start();
+
+    // Enable OS cursor
+    GetSubsystem<Input>()->SetMouseVisible(true);
+
+    // Load strings from JSON files and subscribe to the change language event
+    InitLocalizationSystem();
+
+    // Init the 3D space
+    CreateScene();
+
+    // Init the user interface
+    CreateGUI();
+}
+
+void L10n::InitLocalizationSystem()
+{
+    Localization* l10n = GetSubsystem<Localization>();
+    // JSON files must be in UTF8 encoding without BOM
+    // The first founded language will be set as current
+    l10n->LoadJSONFile("StringsEnRu.json");
+    // You can load multiple files
+    l10n->LoadJSONFile("StringsDe.json");
+    // Hook up to the change language
+    SubscribeToEvent(E_CHANGELANGUAGE, HANDLER(L10n, HandleChangeLanguage));
+}
+
+void L10n::CreateGUI()
+{
+    // Get localization subsystem
+    Localization* l10n = GetSubsystem<Localization>();
+    
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    UIElement* root = GetSubsystem<UI>()->GetRoot();
+    root->SetDefaultStyle(cache->GetResource<XMLFile>("UI/DefaultStyle.xml"));
+
+    Window* window = new Window(context_);
+    root->AddChild(window);
+    window->SetMinSize(384, 192);
+    window->SetLayout(LM_VERTICAL, 6, IntRect(6, 6, 6, 6));
+    window->SetAlignment(HA_CENTER, VA_CENTER);
+    window->SetStyleAuto();
+
+    Text* windowTitle = new Text(context_);
+    windowTitle->SetName("WindowTitle");
+    windowTitle->SetStyleAuto();
+    window->AddChild(windowTitle);
+
+    // In this place the current language is "en" because it was found first when loading the JSON files
+    String langName = l10n->GetLanguage();
+    // Languages are numbered in the loading order
+    int langIndex = l10n->GetLanguageIndex(); // == 0 at the beginning
+    // Get string with identifier "title" in the current language
+    String localizedString = l10n->Get("title");
+    // Localization::Get returns String::EMPTY if the id is empty.
+    // Localization::Get returns the id if translation is not found and will be added a warning into the log.
+
+    windowTitle->SetText(localizedString + " (" + String(langIndex) + " " + langName + ")");
+
+    Button* b = new Button(context_);
+    window->AddChild(b);
+    b->SetStyle("Button");
+    b->SetMinHeight(24);
+    
+    Text* t = b->CreateChild<Text>("ButtonTextChangeLang");
+    // The showing text value will automatically change when language is changed
+    t->SetAutoLocalizable(true);
+    // The text value used as a string identifier in this mode.
+    // Remember that a letter case of the id and of the lang name is important.
+    t->SetText("Press this button");
+    
+    t->SetAlignment(HA_CENTER, VA_CENTER);
+    t->SetStyle("Text");
+    SubscribeToEvent(b, E_RELEASED, HANDLER(L10n, HandleChangeLangButtonPressed));
+
+    b = new Button(context_);
+    window->AddChild(b);
+    b->SetStyle("Button");
+    b->SetMinHeight(24);
+    t = b->CreateChild<Text>("ButtonTextQuit");
+    t->SetAlignment(HA_CENTER, VA_CENTER);
+    t->SetStyle("Text");
+    
+    // Manually set text in the current language
+    t->SetText(l10n->Get("quit"));
+    
+    SubscribeToEvent(b, E_RELEASED, HANDLER(L10n, HandleQuitButtonPressed));
+}
+
+void L10n::CreateScene()
+{
+    // Get localization subsystem
+    Localization* l10n = GetSubsystem<Localization>();
+
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    scene_ = new Scene(context_);
+    scene_->CreateComponent<Octree>();
+    
+    Zone* zone = scene_->CreateComponent<Zone>();
+    zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.0f));
+    zone->SetAmbientColor(Color(0.5f, 0.5f, 0.5f));
+    zone->SetFogColor(Color(0.4f, 0.5f, 0.8f));
+    zone->SetFogStart(1.0f);
+    zone->SetFogEnd(100.0f);
+
+    Node* planeNode = scene_->CreateChild("Plane");
+    planeNode->SetScale(Vector3(300.0f, 1.0f, 300.0f));
+    StaticModel* planeObject = planeNode->CreateComponent<StaticModel>();
+    planeObject->SetModel(cache->GetResource<Model>("Models/Plane.mdl"));
+    planeObject->SetMaterial(cache->GetResource<Material>("Materials/StoneTiled.xml"));
+
+    Node* lightNode = scene_->CreateChild("DirectionalLight");
+    lightNode->SetDirection(Vector3(0.6f, -1.0f, 0.8f));
+    Light* light = lightNode->CreateComponent<Light>();
+    light->SetLightType(LIGHT_DIRECTIONAL);
+    light->SetColor(Color(0.8f, 0.8f, 0.8f));
+
+    cameraNode_ = scene_->CreateChild("Camera");
+    cameraNode_->CreateComponent<Camera>();
+    cameraNode_->SetPosition(Vector3(0.0f, 10.0f, -30.0f));
+
+    Node* text3DNode = scene_->CreateChild("Text3D");
+    text3DNode->SetPosition(Vector3(0.0f, 0.1f, 30.0f));
+    Text3D* text3D = text3DNode->CreateComponent<Text3D>();
+
+    // Manually set text in the current language.
+    text3D->SetText(l10n->Get("lang"));
+
+    text3D->SetFont(cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 30);
+    text3D->SetColor(Color::BLACK);
+    text3D->SetAlignment(HA_CENTER, VA_BOTTOM);
+    text3DNode->SetScale(15);
+
+    Renderer* renderer = GetSubsystem<Renderer>();
+    SharedPtr<Viewport> viewport(new Viewport(context_, scene_, cameraNode_->GetComponent<Camera>()));
+    renderer->SetViewport(0, viewport);
+
+    SubscribeToEvent(E_UPDATE, HANDLER(L10n, HandleUpdate));
+}
+
+void L10n::HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    using namespace Update;
+    float timeStep = eventData[P_TIMESTEP].GetFloat();
+    Input* input = GetSubsystem<Input>();
+    const float MOUSE_SENSITIVITY = 0.1f;
+    IntVector2 mouseMove = input->GetMouseMove();
+    yaw_ += MOUSE_SENSITIVITY * mouseMove.x_;
+    pitch_ += MOUSE_SENSITIVITY * mouseMove.y_;
+    pitch_ = Clamp(pitch_, -90.0f, 90.0f);
+    cameraNode_->SetRotation(Quaternion(pitch_, yaw_, 0.0f));
+}
+
+void L10n::HandleChangeLangButtonPressed(StringHash eventType, VariantMap& eventData)
+{
+    Localization* l10n = GetSubsystem<Localization>();
+    // Languages are numbered in the loading order
+    int lang = l10n->GetLanguageIndex();
+    lang++;
+    if (lang >= l10n->GetNumLanguages())
+        lang = 0;
+    l10n->SetLanguage(lang);
+}
+
+void L10n::HandleQuitButtonPressed(StringHash eventType, VariantMap& eventData)
+{
+    engine_->Exit();
+}
+
+// You can manually change texts, sprites and other aspects of the game when language is changed
+void L10n::HandleChangeLanguage(StringHash eventType, VariantMap& eventData)
+{
+    Localization* l10n = GetSubsystem<Localization>();
+    UIElement* uiRoot = GetSubsystem<UI>()->GetRoot();
+
+    Text* windowTitle = static_cast<Text*>(uiRoot->GetChild("WindowTitle", true));
+    windowTitle->SetText(l10n->Get("title") + " (" + String(l10n->GetLanguageIndex()) + " " + l10n->GetLanguage() + ")");
+
+    Text* buttonText = static_cast<Text*>(uiRoot->GetChild("ButtonTextQuit", true));
+    buttonText->SetText(l10n->Get("quit"));
+
+    Text3D* text3D = scene_->GetChild("Text3D")->GetComponent<Text3D>();
+    text3D->SetText(l10n->Get("lang"));
+
+    // A text on the button "Press this button" changes automatically
+}

+ 59 - 0
Source/Samples/40_Localization/L10n.h

@@ -0,0 +1,59 @@
+//
+// Copyright (c) 2008-2015 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "Sample.h"
+
+/// Localization example.
+/// This sample demonstrates:
+///     - Loading a collection of strings from JSON-files
+///     - Creating text elements that automatically translates itself by changing the language
+///     - The manually reaction to change language
+class L10n : public Sample
+{
+    OBJECT(L10n);
+
+public:
+    /// Construct.
+    L10n(Context* context);
+
+    /// Setup after engine initialization and before running the main loop.
+    virtual void Start();
+
+private:
+    // Load strings from JSON files and subscribe to the change language event
+    void InitLocalizationSystem();
+    // Init the 3D space
+    void CreateScene();
+    // Init the user interface
+    void CreateGUI();
+    /// Handle the logic update event.
+    void HandleUpdate(StringHash eventType, VariantMap& eventData);
+    // Handle the change language event.
+    void HandleChangeLanguage(StringHash eventType, VariantMap& eventData);
+    // Hook up to the buttons pressing
+    void HandleChangeLangButtonPressed(StringHash eventType, VariantMap& eventData);
+    void HandleQuitButtonPressed(StringHash eventType, VariantMap& eventData);
+};
+
+

+ 2 - 0
Source/Samples/CMakeLists.txt

@@ -94,3 +94,5 @@ if (URHO3D_URHO2D)
     add_sample_subdirectory (33_Urho2DSpriterAnimation)
     add_sample_subdirectory (36_Urho2DTileMap)
 endif ()
+add_sample_subdirectory (40_Localization)
+

+ 2 - 0
Source/Urho3D/Engine/Engine.cpp

@@ -47,6 +47,7 @@
 #include "../Physics/PhysicsWorld.h"
 #endif
 #include "../Resource/ResourceCache.h"
+#include "../Resource/Localization.h"
 #include "../Scene/Scene.h"
 #include "../Scene/SceneEvents.h"
 #include "../UI/UI.h"
@@ -119,6 +120,7 @@ Engine::Engine(Context* context) :
     context_->RegisterSubsystem(new Log(context_));
 #endif
     context_->RegisterSubsystem(new ResourceCache(context_));
+    context_->RegisterSubsystem(new Localization(context_));
 #ifdef URHO3D_NETWORK
     context_->RegisterSubsystem(new Network(context_));
 #endif

+ 36 - 0
Source/Urho3D/LuaScript/pkgs/Resource/Localization.pkg

@@ -0,0 +1,36 @@
+$#include "Resource/Localization.h"
+
+class Localization : public Object
+{
+    int GetNumLanguages() const;
+    int GetLanguageIndex() const;
+    int GetLanguageIndex(const String &language);
+    String GetLanguage();
+    String GetLanguage(int index);
+    void SetLanguage(int index);
+    void SetLanguage(const String &language);
+    String Get(const String &id);
+    void Reset();
+    void LoadJSON(const JSONValue &source);
+    void LoadJSONFile(const String &name);
+
+    tolua_readonly tolua_property__get_set int numLanguages;
+    tolua_readonly tolua_property__get_set int languageIndex;
+    tolua_readonly tolua_property__get_set String language;
+};
+
+Localization* GetLocalization();
+tolua_readonly tolua_property__get_set Localization* localization;
+
+${
+
+#define TOLUA_DISABLE_tolua_ResourceLuaAPI_GetLocalization00
+static int tolua_ResourceLuaAPI_GetLocalization00(lua_State* tolua_S)
+{
+    return ToluaGetSubsystem<Localization>(tolua_S);
+}
+
+#define TOLUA_DISABLE_tolua_get_localization_ptr
+#define tolua_get_localization_ptr tolua_ResourceLuaAPI_GetLocalization00
+
+$}

+ 1 - 0
Source/Urho3D/LuaScript/pkgs/ResourceLuaAPI.pkg

@@ -5,6 +5,7 @@ $pfile "Resource/JSONFile.pkg"
 $pfile "Resource/XMLElement.pkg"
 $pfile "Resource/XMLFile.pkg"
 $pfile "Resource/ResourceCache.pkg"
+$pfile "Resource/Localization.pkg"
 
 $using namespace Urho3D;
 $#pragma warning(disable:4800)

+ 4 - 0
Source/Urho3D/LuaScript/pkgs/UI/Text.pkg

@@ -27,6 +27,9 @@ class Text : public UIElement
     void SetTextEffect(TextEffect textEffect);
     void SetEffectColor(const Color& effectColor);
     
+    bool GetAutoLocalizable() const;
+    void SetAutoLocalizable(bool enable);
+    
     Font* GetFont() const;
     int GetFontSize() const;
     const String GetText() const;
@@ -55,6 +58,7 @@ class Text : public UIElement
     tolua_property__get_set HorizontalAlignment textAlignment;
     tolua_property__get_set float rowSpacing;
     tolua_property__get_set bool wordwrap;
+    tolua_property__get_set bool autoLocalizable;
     tolua_readonly tolua_property__get_set unsigned selectionStart;
     tolua_readonly tolua_property__get_set unsigned selectionLength;
     tolua_property__get_set Color& selectionColor;

+ 186 - 0
Source/Urho3D/Resource/Localization.cpp

@@ -0,0 +1,186 @@
+//
+// Copyright (c) 2008-2015 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../Precompiled.h"
+
+#include "../Resource/Localization.h"
+#include "../Resource/ResourceCache.h"
+#include "../Resource/JSONFile.h"
+#include "../Resource/ResourceEvents.h"
+#include "../IO/Log.h"
+
+#include "../DebugNew.h"
+
+namespace Urho3D
+{
+
+Localization::Localization(Context* context) :
+    Object(context),
+    languageIndex_(-1)
+{
+}
+
+Localization::~Localization()
+{
+}
+
+int Localization::GetLanguageIndex(const String &language)
+{
+    if (language.Empty())
+    {
+        LOGWARNING("Localization::GetLanguageIndex(language): language name is empty");
+        return -1;
+    }
+    if (GetNumLanguages() == 0)
+    {
+        LOGWARNING("Localization::GetLanguageIndex(language): no loaded languages");
+        return -1;
+    }
+    for (int i = 0; i < GetNumLanguages(); i++)
+    {
+        if (languages_[i] == language)
+            return i;
+    }
+    return -1;
+}
+
+String Localization::GetLanguage()
+{
+    if (languageIndex_ == -1)
+    {
+        LOGWARNING("Localization::GetLanguage(): no loaded languages");
+        return String::EMPTY;
+    }
+    return languages_[languageIndex_];
+}
+
+String Localization::GetLanguage(int index)
+{
+    if (GetNumLanguages() == 0)
+    {
+        LOGWARNING("Localization::GetLanguage(index): no loaded languages");
+        return String::EMPTY;
+    }
+    if (index < 0 || index >= GetNumLanguages())
+    {
+        LOGWARNING("Localization::GetLanguage(index): index out of range");
+        return String::EMPTY;
+    }
+    return languages_[index];
+}
+
+void Localization::SetLanguage(int index)
+{
+    if (GetNumLanguages() == 0)
+    {
+        LOGWARNING("Localization::SetLanguage(index): no loaded languages");
+        return;
+    }
+    if (index < 0 || index >= GetNumLanguages())
+    {
+        LOGWARNING("Localization::SetLanguage(index): index out of range");
+        return;
+    }
+    if (index != languageIndex_)
+    {
+        languageIndex_ = index;
+        VariantMap& eventData = GetEventDataMap();
+        SendEvent(E_CHANGELANGUAGE, eventData);
+    }
+}
+
+void Localization::SetLanguage(const String &language)
+{
+    if (language.Empty())
+    {
+        LOGWARNING("Localization::SetLanguage(language): language name is empty");
+        return;
+    }
+    if (GetNumLanguages() == 0)
+    {
+        LOGWARNING("Localization::SetLanguage(language): no loaded languages");
+        return;
+    }
+    int index = GetLanguageIndex(language);
+    if (index == -1)
+    {
+        LOGWARNING("Localization::SetLanguage(language): language not found");
+        return;
+    }
+    SetLanguage(index);
+}
+
+String Localization::Get(const String &id)
+{
+    if (id.Empty())
+        return String::EMPTY;
+    if (GetNumLanguages() == 0)
+    {
+        LOGWARNING("Localization::Get(id): no loaded languages");
+        return id;
+    }
+    String result = strings_[StringHash(GetLanguage())][StringHash(id)];
+    if (result.Empty())
+    {
+        LOGWARNING("Localization::Get(\"" + id + "\") not found translation, language=\"" + GetLanguage() + "\"");
+        return id;
+    }
+    return result;
+}
+
+void Localization::Reset()
+{
+    languages_.Clear();
+    languageIndex_ = -1;
+    strings_.Clear();
+}
+
+void Localization::LoadJSON(const JSONValue &source)
+{
+    Vector<String> ids = source.GetChildNames();
+    for (unsigned i = 0; i < ids.Size(); i++)
+    {
+        String id = ids[i];
+        JSONValue value = source.GetChild(id);
+        Vector<String> langs = value.GetValueNames();
+        for (unsigned j = 0; j < langs.Size(); j++)
+        {
+            String lang = langs[j];
+            String string = value.GetString(lang);
+            strings_[StringHash(lang)][StringHash(id)] = string;
+            if (!languages_.Contains(lang))
+                languages_.Push(lang);
+            if (languageIndex_ == -1)
+                languageIndex_ = 0;
+        }
+    }
+}
+
+void Localization::LoadJSONFile(const String &name)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    JSONFile* jsonFile = cache->GetResource<JSONFile>(name);
+    if (jsonFile)
+        LoadJSON(jsonFile->GetRoot());
+}
+
+}

+ 73 - 0
Source/Urho3D/Resource/Localization.h

@@ -0,0 +1,73 @@
+//
+// Copyright (c) 2008-2015 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "../Core/Context.h"
+#include "../Resource/JSONValue.h"
+
+namespace Urho3D
+{
+
+/// %Localization subsystem. Stores all the strings in all languages.
+class URHO3D_API Localization : public Object
+{
+    OBJECT(Localization);
+
+public:
+    /// Construct.
+    Localization(Context* context);
+    /// Destruct. Free all resources.
+    virtual ~Localization();
+    /// Return the number of languages.
+    int GetNumLanguages() const { return (int)languages_.Size(); }
+    /// Return the index number of current language. The index is determined by the order of loading.
+    int GetLanguageIndex() const { return languageIndex_; }
+    /// Return the index number of language. The index is determined by the order of loading.
+    int GetLanguageIndex(const String &language);
+    /// Return the name of current language.
+    String GetLanguage();
+    /// Return the name of language.
+    String GetLanguage(int index);
+    /// Set current language.
+    void SetLanguage(int index);
+    /// Set current language.
+    void SetLanguage(const String &language);
+    /// Return a string in the current language. Returns String::EMPTY if id is empty. Returns id if translation is not found.
+    String Get(const String &id);
+    /// Clear all loaded strings.
+    void Reset();
+    /// Load strings from JSONValue.
+    void LoadJSON(const JSONValue &source);
+    /// Load strings from JSONFile. The file should be UTF8 without BOM.
+    void LoadJSONFile(const String &name);
+
+private:
+    /// Language names.
+    Vector<String> languages_;
+    /// Index of current language.
+    int languageIndex_;
+    /// Storage strings: <Language <StringId, Value> >.
+    HashMap<StringHash, HashMap<StringHash, String> > strings_;
+};
+
+}

+ 5 - 0
Source/Urho3D/Resource/ResourceEvents.h

@@ -75,4 +75,9 @@ EVENT(E_RESOURCEBACKGROUNDLOADED, ResourceBackgroundLoaded)
     PARAM(P_RESOURCE, Resource);                    // Resource pointer
 }
 
+/// Language changed.
+EVENT(E_CHANGELANGUAGE, ChangeLanguage)
+{
+}
+
 }

+ 24 - 0
Source/Urho3D/Script/ResourceAPI.cpp

@@ -26,6 +26,7 @@
 #include "../Resource/Image.h"
 #include "../Resource/JSONFile.h"
 #include "../Resource/ResourceCache.h"
+#include "../Resource/Localization.h"
 #include "../Script/APITemplates.h"
 
 namespace Urho3D
@@ -103,6 +104,28 @@ static bool ResourceCacheBackgroundLoadResource(const String& type, const String
     return ptr->BackgroundLoadResource(type, name, sendEventOnFailure);
 }
 
+static Localization* GetLocalization()
+{
+    return GetScriptContext()->GetSubsystem<Localization>();
+}
+
+static void RegisterLocalization(asIScriptEngine* engine)
+{
+    RegisterObject<Localization>(engine, "Localization");
+    engine->RegisterObjectMethod("Localization", "int get_numLanguages() const", asMETHOD(Localization, GetNumLanguages), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "int get_languageIndex() const", asMETHODPR(Localization, GetLanguageIndex, () const, int), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "int GetLanguageIndex(const String&in)", asMETHODPR(Localization, GetLanguageIndex, (const String&), int), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "String get_language()", asMETHODPR(Localization, GetLanguage, (), String), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "String GetLanguage(int)", asMETHODPR(Localization, GetLanguage, (int), String), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "void SetLanguage(int)", asMETHODPR(Localization, SetLanguage, (int), void), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "void SetLanguage(const String&in)", asMETHODPR(Localization, SetLanguage, (const String&), void), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "String Get(const String&in)", asMETHOD(Localization, Get), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "void Reset()", asMETHOD(Localization, Reset), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "void LoadJSON(const JSONValue&in)", asMETHOD(Localization, LoadJSON), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Localization", "void LoadJSONFile(const String&in)", asMETHOD(Localization, LoadJSONFile), asCALL_THISCALL);
+    engine->RegisterGlobalFunction("Localization@+ get_localization()", asFUNCTION(GetLocalization), asCALL_CDECL);
+}
+
 static void RegisterResourceCache(asIScriptEngine* engine)
 {
     RegisterObject<ResourceCache>(engine, "ResourceCache");
@@ -562,6 +585,7 @@ void RegisterResourceAPI(asIScriptEngine* engine)
     RegisterJSONFile(engine);
     RegisterXMLElement(engine);
     RegisterXMLFile(engine);
+    RegisterLocalization(engine);
 }
 
 }

+ 2 - 0
Source/Urho3D/Script/UIAPI.cpp

@@ -376,6 +376,8 @@ static void RegisterText(asIScriptEngine* engine)
     engine->RegisterObjectMethod("Text", "float get_rowSpacing() const", asMETHOD(Text, GetRowSpacing), asCALL_THISCALL);
     engine->RegisterObjectMethod("Text", "void set_wordwrap(bool)", asMETHOD(Text, SetWordwrap), asCALL_THISCALL);
     engine->RegisterObjectMethod("Text", "bool get_wordwrap() const", asMETHOD(Text, GetWordwrap), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "void set_autoLocalizable(bool)", asMETHOD(Text, SetAutoLocalizable), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "bool get_autoLocalizable() const", asMETHOD(Text, GetAutoLocalizable), asCALL_THISCALL);
     engine->RegisterObjectMethod("Text", "uint get_selectionStart() const", asMETHOD(Text, GetSelectionStart), asCALL_THISCALL);
     engine->RegisterObjectMethod("Text", "uint get_selectionLength() const", asMETHOD(Text, GetSelectionLength), asCALL_THISCALL);
     engine->RegisterObjectMethod("Text", "void set_selectionColor(const Color&in)", asMETHOD(Text, SetSelectionColor), asCALL_THISCALL);

+ 54 - 8
Source/Urho3D/UI/Text.cpp

@@ -30,6 +30,8 @@
 #include "../UI/Font.h"
 #include "../UI/FontFace.h"
 #include "../UI/Text.h"
+#include "../Resource/Localization.h"
+#include "../Resource/ResourceEvents.h"
 
 #include "../DebugNew.h"
 
@@ -56,6 +58,7 @@ Text::Text(Context* context) :
     textAlignment_(HA_LEFT),
     rowSpacing_(1.0f),
     wordWrap_(false),
+    autoLocalizable_(false),
     charLocationsDirty_(true),
     selectionStart_(0),
     selectionLength_(0),
@@ -86,6 +89,7 @@ void Text::RegisterObject(Context* context)
     ENUM_ATTRIBUTE("Text Alignment", textAlignment_, horizontalAlignments, HA_LEFT, AM_FILE);
     ATTRIBUTE("Row Spacing", float, rowSpacing_, 1.0f, AM_FILE);
     ATTRIBUTE("Word Wrap", bool, wordWrap_, false, AM_FILE);
+    ACCESSOR_ATTRIBUTE("Auto Localizable", GetAutoLocalizable, SetAutoLocalizable, bool, false, AM_FILE);
     ACCESSOR_ATTRIBUTE("Selection Color", GetSelectionColor, SetSelectionColor, Color, Color::TRANSPARENT, AM_FILE);
     ACCESSOR_ATTRIBUTE("Hover Color", GetHoverColor, SetHoverColor, Color, Color::TRANSPARENT, AM_FILE);
     ENUM_ATTRIBUTE("Text Effect", textEffect_, textEffects, TE_NONE, AM_FILE);
@@ -99,10 +103,7 @@ void Text::ApplyAttributes()
 {
     UIElement::ApplyAttributes();
 
-    // Decode to Unicode now
-    unicodeText_.Clear();
-    for (unsigned i = 0; i < text_.Length();)
-        unicodeText_.Push(text_.NextUTF8Char(i));
+    DecodeToUnicode();
 
     fontSize_ = Max(fontSize_, 1);
     ValidateSelection();
@@ -252,15 +253,27 @@ bool Text::SetFont(Font* font, int size)
     return true;
 }
 
-void Text::SetText(const String& text)
+void Text::DecodeToUnicode()
 {
-    text_ = text;
-
-    // Decode to Unicode now
     unicodeText_.Clear();
     for (unsigned i = 0; i < text_.Length();)
         unicodeText_.Push(text_.NextUTF8Char(i));
+}
 
+void Text::SetText(const String& text)
+{
+    if (autoLocalizable_)
+    {
+        stringId_ = text;
+        Localization* l10n = GetSubsystem<Localization>();
+        text_ = l10n->Get(stringId_);
+    }
+    else
+    {
+        text_ = text;
+    }
+
+    DecodeToUnicode();
     ValidateSelection();
     UpdateText();
 }
@@ -292,6 +305,39 @@ void Text::SetWordwrap(bool enable)
     }
 }
 
+void Text::SetAutoLocalizable(bool enable)
+{
+    if (enable != autoLocalizable_)
+    {
+        autoLocalizable_ = enable;
+        if (enable)
+        {
+            stringId_ = text_;
+            Localization* l10n = GetSubsystem<Localization>();
+            text_ = l10n->Get(stringId_);
+            SubscribeToEvent(E_CHANGELANGUAGE, HANDLER(Text, HandleChangeLanguage));
+        }
+        else
+        {
+            text_ = stringId_;
+            stringId_ = "";
+            UnsubscribeFromEvent(E_CHANGELANGUAGE);
+        }
+        DecodeToUnicode();
+        ValidateSelection();
+        UpdateText();
+    }
+}
+
+void Text::HandleChangeLanguage(StringHash eventType, VariantMap& eventData)
+{
+    Localization* l10n = GetSubsystem<Localization>();
+    text_ = l10n->Get(stringId_);
+    DecodeToUnicode();
+    ValidateSelection();
+    UpdateText();
+}
+
 void Text::SetSelection(unsigned start, unsigned length)
 {
     selectionStart_ = start;

+ 13 - 0
Source/Urho3D/UI/Text.h

@@ -105,6 +105,8 @@ public:
     void SetRowSpacing(float spacing);
     /// Set wordwrap. In wordwrap mode the text element will respect its current width. Otherwise it resizes itself freely.
     void SetWordwrap(bool enable);
+    /// The text will be automatically translated. The text value used as string identifier.
+    void SetAutoLocalizable(bool enable);
     /// Set selection. When length is not provided, select until the text ends.
     void SetSelection(unsigned start, unsigned length = M_MAX_UNSIGNED);
     /// Clear selection.
@@ -136,6 +138,9 @@ public:
     /// Return wordwrap mode.
     bool GetWordwrap() const { return wordWrap_; }
 
+    /// Return auto localizable mode.
+    bool GetAutoLocalizable() const { return autoLocalizable_; }
+
     /// Return selection start.
     unsigned GetSelectionStart() const { return selectionStart_; }
 
@@ -245,6 +250,14 @@ protected:
     Vector<PODVector<GlyphLocation> > pageGlyphLocations_;
     /// Cached locations of each character in the text.
     PODVector<CharLocation> charLocations_;
+    /// The text will be automatically translated.
+    bool autoLocalizable_;
+    /// Storage string id. Used when enabled autoLocalizable.
+    String stringId_;
+    /// Handle change Language.
+    void HandleChangeLanguage(StringHash eventType, VariantMap& eventData);
+    /// UTF8 to Unicode.
+    void DecodeToUnicode();
 };
 
 }

+ 310 - 0
bin/Data/EditorStrings.json

@@ -0,0 +1,310 @@
+{
+	"Open scene...":{
+		"en":"Open scene...",
+		"ru":"Открыть сцену..."
+	},
+	"Save scene":{
+		"en":"Save scene",
+		"ru":"Сохранить сцену"
+	},
+	"Save scene as...":{
+		"en":"Save scene as...",
+		"ru":"Сохранить сцену как..."
+	},
+	"Open recent scene":{
+		"en":"Open recent scene",
+		"ru":"Открыть прежнюю сцену"
+	},
+	"Load node":{
+		"en":"Load node",
+		"ru":"Загрузить ноду"
+	},
+	"As replicated...":{
+		"en":"As replicated...",
+		"ru":"Реплицируемую..."
+	},
+	"As local...":{
+		"en":"As local...",
+		"ru":"Локальную..."
+	},
+	"Save node as...":{
+		"en":"Save node as...",
+		"ru":"Сохранить ноду как..."
+	},
+	"Import model...":{
+		"en":"Import model...",
+		"ru":"Импортировать модель..."
+	},
+	"Import scene...":{
+		"en":"Import scene...",
+		"ru":"Импортировать сцену..."
+	},
+	"Run script...":{
+		"en":"Run script...",
+		"ru":"Запустить скрипт..."
+	},
+	"Set resource path...":{
+		"en":"Set resource path...",
+		"ru":"Указать путь к ресурсам..."
+	},
+	"Exit":{
+		"en":"Exit",
+		"ru":"Выход"
+	},
+	"Edit":{
+		"en":"Edit",
+		"ru":"Редактирование"
+	},
+	"Undo":{
+		"en":"Undo",
+		"ru":"Отменить"
+	},
+	"Redo":{
+		"en":"Redo",
+		"ru":"Повторить"
+	},
+	"Cut":{
+		"en":"Cut",
+		"ru":"Вырезать"
+	},
+	"Duplicate":{
+		"en":"Duplicate",
+		"ru":"Дублировать"
+	},
+	"Copy":{
+		"en":"Copy",
+		"ru":"Копировать"
+	},
+	"Paste":{
+		"en":"Paste",
+		"ru":"Вставить"
+	},
+	"Delete":{
+		"en":"Delete",
+		"ru":"Удалить"
+	},
+	"Select all":{
+		"en":"Select all",
+		"ru":"Выделить всё"
+	},
+	"Deselect all":{
+		"en":"Deselect all",
+		"ru":"Снять все выделения"
+	},
+	"Reset to default":{
+		"en":"Reset to default",
+		"ru":"Сбросить"
+	},
+	"Reset position":{
+		"en":"Reset position",
+		"ru":"Сбросить положение"
+	},
+	"Reset rotation":{
+		"en":"Reset rotation",
+		"ru":"Сбросить поворот"
+	},
+	"Reset scale":{
+		"en":"Reset scale",
+		"ru":"Сбросить масштаб"
+	},
+	"Enable/disable":{
+		"en":"Enable/disable",
+		"ru":"Включить/Отключить"
+	},
+	"Unparent":{
+		"en":"Unparent",
+		"ru":"Разорвать родительскую связь"
+	},
+	"Toggle update":{
+		"en":"Toggle update",
+		"ru":"Вкл/откл обновление"
+	},
+	"Stop test animation":{
+		"en":"Stop test animation",
+		"ru":"Остановить тестовую анимацию"
+	},
+	"Rebuild navigation data":{
+		"en":"Rebuild navigation data",
+		"ru":"Реконструировать навигационные данные"
+	},
+	"Add children to SM-group":{
+		"en":"Add children to SM-group",
+		"ru":"Добавить потомка к StaticModelGroup"
+	},
+	"Set children as spline path":{
+		"en":"Set children as spline path",
+		"ru":"Установить потомка как сплайновый путь"
+	},
+	"Non-cyclic":{
+		"en":"Non-cyclic",
+		"ru":"Нецикличный"
+	},
+	"Cyclic":{
+		"en":"Cyclic",
+		"ru":"Цикличный"
+	},
+	"Create":{
+		"en":"Create",
+		"ru":"Создать"
+	},
+	"Replicated node":{
+		"en":"Replicated node",
+		"ru":"Реплицируемую ноду"
+	},
+	"Local node":{
+		"en":"Local node",
+		"ru":"Локальную ноду"
+	},
+	"Component":{
+		"en":"Component",
+		"ru":"Компонент"
+	},
+	"Audio":{
+		"en":"Audio",
+		"ru":"Аудио"
+	},
+	"Geometry":{
+		"en":"Geometry",
+		"ru":"Геометрия"
+	},
+	"Logic":{
+		"en":"Logic",
+		"ru":"Логика"
+	},
+	"Navigation":{
+		"en":"Navigation",
+		"ru":"Навигация"
+	},
+	"Network":{
+		"en":"Network",
+		"ru":"Сеть"
+	},
+	"Physics":{
+		"en":"Physics",
+		"ru":"Физика"
+	},
+	"Scene":{
+		"en":"Scene",
+		"ru":"Сцена"
+	},
+	"Subsystem":{
+		"en":"Subsystem",
+		"ru":"Подсистема"
+	},
+	"Urho2D":{
+		"en":"Urho2D",
+		"ru":"Urho2D"
+	},
+	"Builtin object":{
+		"en":"Builtin object",
+		"ru":"Встроенный объект"
+	},
+	"UI-element":{
+		"en":"UI-element",
+		"ru":"Элемент интерфейса"
+	},
+	"UI-layout":{
+		"en":"UI-layout",
+		"ru":"Разметка интерфейса"
+	},
+	"Open UI-layout...":{
+		"en":"Open UI-layout...",
+		"ru":"Открыть разметку..."
+	},
+	"Save UI-layout":{
+		"en":"Save UI-layout",
+		"ru":"Сохранить разметку"
+	},
+	"Save UI-layout as...":{
+		"en":"Save UI-layout as...",
+		"ru":"Сохранить разметку как..."
+	},
+	"Close UI-layout":{
+		"en":"Close UI-layout",
+		"ru":"Закрыть разметку"
+	},
+	"Close all UI-layouts":{
+		"en":"Close all UI-layouts",
+		"ru":"Закрыть все разметки"
+	},
+	"Load child element...":{
+		"en":"Load child element...",
+		"ru":"Загрузить дочерний элемент..."
+	},
+	"Save child element as...":{
+		"en":"Save child element as...",
+		"ru":"Сохранить дочерний элемент как..."
+	},
+	"Set default style...":{
+		"en":"Set default style...",
+		"ru":"Установить стандартный стиль..."
+	},
+	"View":{
+		"en":"View",
+		"ru":"Вид"
+	},
+	"Hierarchy":{
+		"en":"Hierarchy",
+		"ru":"Иерархия"
+	},
+	"Attribute inspector":{
+		"en":"Attribute inspector",
+		"ru":"Аттрибуты"
+	},
+	"Material editor":{
+		"en":"Material editor",
+		"ru":"Редактор материалов"
+	},
+	"Particle editor":{
+		"en":"Particle editor",
+		"ru":"Редактор частиц"
+	},
+	"Spawn editor":{
+		"en":"Spawn editor",
+		"ru":"Редактор спауна"
+	},
+	"Sound Type editor":{
+		"en":"Sound Type editor",
+		"ru":"Тип звука"
+	},
+	"Editor settings":{
+		"en":"Editor settings",
+		"ru":"Настройки редактора"
+	},
+	"Editor preferences":{
+		"en":"Editor preferences",
+		"ru":"Установки редактора"
+	},
+	"Hide editor":{
+		"en":"Hide editor",
+		"ru":"Скрыть редактор"
+	},
+	"Resource Browser":{
+		"en":"Resource Browser",
+		"ru":"Браузер ресурсов"
+	},
+	"New scene":{
+		"en":"New scene",
+		"ru":"Создать новую сцену"
+	},
+	"Files left to scan: ":{
+		"en":"Files left to scan: ",
+		"ru":"Файлов до конца сканирования: "
+	},
+	"Scan complete":{
+		"en":"Scan complete",
+		"ru":"Сканирование окончено"
+	},
+	"Root":{
+		"en":"Root",
+		"ru":"Корень"
+	},
+	"Showing files: ":{
+		"en":"Showing files: ",
+		"ru":"Показано файлов: "
+	},
+	"File":{
+		"en":"File",
+		"ru":"Файл"
+	}
+}

+ 176 - 0
bin/Data/LuaScripts/40_Localization.lua

@@ -0,0 +1,176 @@
+-- Localization example.
+-- This sample demonstrates:
+--     - Loading a collection of strings from JSON-files
+--     - Creating text elements that automatically translates itself by changing the language
+--     - The manually reaction to change language
+
+require "LuaScripts/Utilities/Sample"
+
+function Start()
+    -- Execute the common startup for samples
+    SampleStart()
+    
+    -- Enable OS cursor
+    input.mouseVisible = true
+    
+    -- Load strings from JSON files and subscribe to the change language event
+    InitLocalizationSystem()
+
+    -- Init the 3D space
+    CreateScene()
+
+    -- Init the user interface
+    CreateGUI()
+end
+
+function InitLocalizationSystem()
+    -- JSON files must be in UTF8 encoding without BOM
+    -- The first founded language will be set as current
+    localization:LoadJSONFile("StringsEnRu.json")
+    -- You can load multiple files
+    localization:LoadJSONFile("StringsDe.json")
+    -- Hook up to the change language
+    SubscribeToEvent("ChangeLanguage", "HandleChangeLanguage")
+end
+
+function CreateGUI()
+    ui.root.defaultStyle = cache:GetResource("XMLFile", "UI/DefaultStyle.xml")
+
+    local window = Window:new()
+    ui.root:AddChild(window)
+    window:SetMinSize(384, 192)
+    window:SetLayout(LM_VERTICAL, 6, IntRect(6, 6, 6, 6))
+    window:SetAlignment(HA_CENTER, VA_CENTER)
+    window:SetStyleAuto()
+
+    local windowTitle = Text:new()
+    windowTitle.name = "WindowTitle"
+    windowTitle:SetStyleAuto()
+    window:AddChild(windowTitle)
+
+    -- In this place the current language is "en" because it was found first when loading the JSON files
+    local langName = localization.language
+    -- Languages are numbered in the loading order
+    local langIndex = localization.languageIndex -- == 0 at the beginning
+    -- Get string with identifier "title" in the current language
+    local localizedString = localization:Get("title")
+    -- Localization:Get returns String.EMPTY if the id is empty.
+    -- Localization:Get returns the id if translation is not found and will be added a warning into the log.
+
+    windowTitle.text = localizedString .. " (" .. langIndex .. " " .. langName .. ")"
+
+    local b = Button:new()
+    window:AddChild(b)
+    b:SetStyle("Button")
+    b.minHeight = 24
+    
+    local t = b:CreateChild("Text", "ButtonTextChangeLang")
+    -- The showing text value will automatically change when language is changed
+    t.autoLocalizable = true
+    -- The text value used as a string identifier in this mode.
+    -- Remember that a letter case of the id and of the lang name is important.
+    t.text = "Press this button"
+    
+    t:SetAlignment(HA_CENTER, VA_CENTER)
+    t:SetStyle("Text")
+    SubscribeToEvent(b, "Released", "HandleChangeLangButtonPressed")
+
+    b = Button:new()
+    window:AddChild(b)
+    b:SetStyle("Button")
+    b.minHeight = 24
+    t = b:CreateChild("Text", "ButtonTextQuit")
+    t:SetAlignment(HA_CENTER, VA_CENTER)
+    t:SetStyle("Text")
+    
+    -- Manually set text in the current language
+    t.text = localization:Get("quit")
+    
+    SubscribeToEvent(b, "Released", "HandleQuitButtonPressed")
+end
+
+function CreateScene()
+    scene_ = Scene:new()
+    scene_:CreateComponent("Octree")
+    
+    local zone = scene_:CreateComponent("Zone")
+    zone.boundingBox = BoundingBox:new(-1000.0, 1000.0)
+    zone.ambientColor = Color:new(0.5, 0.5, 0.5)
+    zone.fogColor = Color:new(0.4, 0.5, 0.8)
+    zone.fogStart = 1.0
+    zone.fogEnd = 100.0
+
+    local planeNode = scene_:CreateChild("Plane")
+    planeNode.scale = Vector3:new(300.0, 1.0, 300.0)
+    local planeObject = planeNode:CreateComponent("StaticModel")
+    planeObject.model = cache:GetResource("Model", "Models/Plane.mdl")
+    planeObject.material = cache:GetResource("Material", "Materials/StoneTiled.xml")
+
+    local lightNode = scene_:CreateChild("DirectionalLight")
+    lightNode.direction = Vector3:new(0.6, -1.0, 0.8)
+    local light = lightNode:CreateComponent("Light")
+    light.lightType = LIGHT_DIRECTIONAL
+    light.color = Color:new(0.8, 0.8, 0.8)
+
+    cameraNode = scene_:CreateChild("Camera")
+    cameraNode:CreateComponent("Camera")
+    cameraNode.position = Vector3:new(0.0, 10.0, -30.0)
+
+    local text3DNode = scene_:CreateChild("Text3D")
+    text3DNode.position = Vector3:new(0.0, 0.1, 30.0)
+    text3DNode:SetScale(15)
+    local text3D = text3DNode:CreateComponent("Text3D")
+
+    -- Manually set text in the current language.
+    text3D.text = localization:Get("lang")
+
+    text3D:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 30)
+    text3D.color = Color.BLACK
+    text3D:SetAlignment(HA_CENTER, VA_BOTTOM)
+
+    local viewport = Viewport:new(scene_, cameraNode:GetComponent("Camera"))
+    renderer:SetViewport(0, viewport)
+
+    SubscribeToEvent("Update", "HandleUpdate")
+end
+
+function HandleUpdate(eventType, eventData)
+    local timeStep = eventData:GetFloat("TimeStep")
+    local MOUSE_SENSITIVITY = 0.1
+    local mouseMove = input.mouseMove
+    yaw = yaw + MOUSE_SENSITIVITY * mouseMove.x
+    pitch = pitch + MOUSE_SENSITIVITY * mouseMove.y
+    pitch = Clamp(pitch, -90.0, 90.0)
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0)
+end
+
+function HandleChangeLangButtonPressed(eventType, eventData)
+    -- Languages are numbered in the loading order
+    local lang = localization.languageIndex
+    lang = lang + 1
+    if lang >= localization.numLanguages then
+        lang = 0
+    end
+    localization:SetLanguage(lang)
+end
+
+function HandleQuitButtonPressed(eventType, eventData)
+    engine:Exit()
+end
+
+-- You can manually change texts, sprites and other aspects of the game when language is changed
+function HandleChangeLanguage(eventType, eventData)
+    local windowTitle = ui.root:GetChild("WindowTitle", true);
+    windowTitle.text = localization:Get("title") .. " (" ..
+                           localization.languageIndex .. " " ..
+                           localization.language .. ")"
+
+    local buttonText = ui.root:GetChild("ButtonTextQuit", true)
+    buttonText.text = localization:Get("quit")
+
+    local text3D = scene_:GetChild("Text3D"):GetComponent("Text3D")
+    text3D.text = localization:Get("lang")
+
+    -- A text on the button "Press this button" changes automatically
+end
+

+ 184 - 0
bin/Data/Scripts/40_Localization.as

@@ -0,0 +1,184 @@
+// Localization example.
+// This sample demonstrates:
+//     - Loading a collection of strings from JSON-files
+//     - Creating text elements that automatically translates itself by changing the language
+//     - The manually reaction to change language
+
+#include "Scripts/Utilities/Sample.as"
+
+void Start()
+{
+    // Execute the common startup for samples
+    SampleStart();
+    
+    // Enable OS cursor
+    input.mouseVisible = true;
+    
+    // Load strings from JSON files and subscribe to the change language event
+    InitLocalizationSystem();
+
+    // Init the 3D space
+    CreateScene();
+
+    // Init the user interface
+    CreateGUI();
+}
+
+void InitLocalizationSystem()
+{
+    // JSON files must be in UTF8 encoding without BOM
+    // The first founded language will be set as current
+    localization.LoadJSONFile("StringsEnRu.json");
+    // You can load multiple files
+    localization.LoadJSONFile("StringsDe.json");
+    // Hook up to the change language
+    SubscribeToEvent("ChangeLanguage", "HandleChangeLanguage");
+}
+
+void CreateGUI()
+{
+    ui.root.defaultStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
+
+    Window@ window = Window();
+    ui.root.AddChild(window);
+    window.SetMinSize(384, 192);
+    window.SetLayout(LM_VERTICAL, 6, IntRect(6, 6, 6, 6));
+    window.SetAlignment(HA_CENTER, VA_CENTER);
+    window.SetStyleAuto();
+
+    Text@ windowTitle = Text();
+    windowTitle.name = "WindowTitle";
+    windowTitle.SetStyleAuto();
+    window.AddChild(windowTitle);
+
+    // In this place the current language is "en" because it was found first when loading the JSON files
+    String langName = localization.language;
+    // Languages are numbered in the loading order
+    int langIndex = localization.languageIndex; // == 0 at the beginning
+    // Get string with identifier "title" in the current language
+    String localizedString = localization.Get("title");
+    // Localization.Get returns String::EMPTY if the id is empty.
+    // Localization.Get returns the id if translation is not found and will be added a warning into the log.
+
+    windowTitle.text = localizedString + " (" + String(langIndex) + " " + langName + ")";
+
+    Button@ b = Button();
+    window.AddChild(b);
+    b.SetStyle("Button");
+    b.minHeight = 24;
+    
+    Text@ t = b.CreateChild("Text", "ButtonTextChangeLang");
+    // The showing text value will automatically change when language is changed
+    t.autoLocalizable = true;
+    // The text value used as a string identifier in this mode.
+    // Remember that a letter case of the id and of the lang name is important.
+    t.text = "Press this button";
+    
+    t.SetAlignment(HA_CENTER, VA_CENTER);
+    t.SetStyle("Text");
+    SubscribeToEvent(b, "Released", "HandleChangeLangButtonPressed");
+
+    b = Button();
+    window.AddChild(b);
+    b.SetStyle("Button");
+    b.minHeight = 24;
+    t = b.CreateChild("Text", "ButtonTextQuit");
+    t.SetAlignment(HA_CENTER, VA_CENTER);
+    t.SetStyle("Text");
+    
+    // Manually set text in the current language
+    t.text = localization.Get("quit");
+    
+    SubscribeToEvent(b, "Released", "HandleQuitButtonPressed");
+}
+
+void CreateScene()
+{
+    scene_ = Scene();
+    scene_.CreateComponent("Octree");
+    
+    Zone@ zone = scene_.CreateComponent("Zone");
+    zone.boundingBox = BoundingBox(-1000.0f, 1000.0f);
+    zone.ambientColor = Color(0.5f, 0.5f, 0.5f);
+    zone.fogColor = Color(0.4f, 0.5f, 0.8f);
+    zone.fogStart = 1.0f;
+    zone.fogEnd = 100.0f;
+
+    Node@ planeNode = scene_.CreateChild("Plane");
+    planeNode.scale = Vector3(300.0f, 1.0f, 300.0f);
+    StaticModel@ planeObject = planeNode.CreateComponent("StaticModel");
+    planeObject.model = cache.GetResource("Model", "Models/Plane.mdl");
+    planeObject.material = cache.GetResource("Material", "Materials/StoneTiled.xml");
+
+    Node@ lightNode = scene_.CreateChild("DirectionalLight");
+    lightNode.direction = Vector3(0.6f, -1.0f, 0.8f);
+    Light@ light = lightNode.CreateComponent("Light");
+    light.lightType = LIGHT_DIRECTIONAL;
+    light.color = Color(0.8f, 0.8f, 0.8f);
+
+    cameraNode = scene_.CreateChild("Camera");
+    cameraNode.CreateComponent("Camera");
+    cameraNode.position = Vector3(0.0f, 10.0f, -30.0f);
+
+    Node@ text3DNode = scene_.CreateChild("Text3D");
+    text3DNode.position = Vector3(0.0f, 0.1f, 30.0f);
+    text3DNode.SetScale(15);
+    Text3D@ text3D = text3DNode.CreateComponent("Text3D");
+
+    // Manually set text in the current language.
+    text3D.text = localization.Get("lang");
+
+    text3D.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 30);
+    text3D.color = Color::BLACK;
+    text3D.SetAlignment(HA_CENTER, VA_BOTTOM);
+
+    Viewport@ viewport = Viewport(scene_, cameraNode.GetComponent("Camera"));
+    renderer.viewports[0] = viewport;
+
+    SubscribeToEvent("Update", "HandleUpdate");
+}
+
+void HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    float timeStep = eventData["TimeStep"].GetFloat();
+    const float MOUSE_SENSITIVITY = 0.1f;
+    IntVector2 mouseMove = input.mouseMove;
+    yaw += MOUSE_SENSITIVITY * mouseMove.x;
+    pitch += MOUSE_SENSITIVITY * mouseMove.y;
+    pitch = Clamp(pitch, -90.0f, 90.0f);
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);
+}
+
+void HandleChangeLangButtonPressed(StringHash eventType, VariantMap& eventData)
+{
+    // Languages are numbered in the loading order
+    int lang = localization.languageIndex;
+    lang++;
+    if (lang >= localization.numLanguages)
+        lang = 0;
+    localization.SetLanguage(lang);
+}
+
+void HandleQuitButtonPressed(StringHash eventType, VariantMap& eventData)
+{
+    engine.Exit();
+}
+
+// You can manually change texts, sprites and other aspects of the game when language is changed
+void HandleChangeLanguage(StringHash eventType, VariantMap& eventData)
+{
+    Text@ windowTitle = ui.root.GetChild("WindowTitle", true);
+    windowTitle.text = localization.Get("title") + " (" +
+                           String(localization.languageIndex) + " " +
+                           localization.language + ")";
+
+    Text@ buttonText = ui.root.GetChild("ButtonTextQuit", true);
+    buttonText.text = localization.Get("quit");
+
+    Text3D@ text3D = scene_.GetChild("Text3D").GetComponent("Text3D");
+    text3D.text = localization.Get("lang");
+
+    // A text on the button "Press this button" changes automatically
+}
+
+String patchInstructions = "";

+ 9 - 0
bin/Data/Scripts/Editor.as

@@ -24,6 +24,7 @@ void Start()
 {
     // Assign the value ASAP because configFileName is needed on exit, including exit on error
     configFileName = fileSystem.GetAppPreferencesDir("urho3d", "Editor") + "Config.xml";
+    localization.LoadJSONFile("EditorStrings.json");
 
     if (engine.headless)
     {
@@ -90,6 +91,14 @@ void ParseArguments()
                 break;
             }
         }
+        if (arguments[i].ToLower() == "-language")
+        {
+            if (++i < arguments.length)
+            {
+                localization.SetLanguage(arguments[i]);
+                break;
+            }
+        }
     }
 
     if (!loaded)

+ 4 - 4
bin/Data/Scripts/Editor/EditorResourceBrowser.as

@@ -187,9 +187,9 @@ void DoResourceBrowserWork()
     }
 
     if (browserFilesToScan.length > 0)
-        browserStatusMessage.text = "Files left to scan: " + browserFilesToScan.length;
+        browserStatusMessage.text = localization.Get("Files left to scan: " )+ browserFilesToScan.length;
     else
-        browserStatusMessage.text = "Scan complete";
+        browserStatusMessage.text = localization.Get("Scan complete");
 
 }
 
@@ -282,7 +282,7 @@ void CreateDirList(BrowserDir@ dir, UIElement@ parentUI = null)
     Text@ dirText = Text();
     browserDirList.InsertItem(browserDirList.numItems, dirText, parentUI);
     dirText.style = "FileSelectorListText";
-    dirText.text = dir.resourceKey.empty ? "Root" : dir.name;
+    dirText.text = dir.resourceKey.empty ? localization.Get("Root") : dir.name;
     dirText.name = dir.resourceKey;
     dirText.vars[TEXT_VAR_DIR_ID] = dir.resourceKey;
 
@@ -604,7 +604,7 @@ void PopulateResourceBrowserFilesByDirectory(BrowserDir@ dir)
     browserSearchSortMode = BROWSER_SORT_MODE_ALPHA;
     files.Sort();
     PopulateResourceBrowserResults(files);
-    browserResultsMessage.text = "Showing " + files.length + " files";
+    browserResultsMessage.text = localization.Get("Showing files: ") + files.length;
 }
 
 

+ 22 - 1
bin/Data/Scripts/Editor/EditorUI.as

@@ -92,6 +92,7 @@ void CreateUI()
     SubscribeToEvent("KeyDown", "HandleKeyDown");
     SubscribeToEvent("KeyUp", "UnfadeUI");
     SubscribeToEvent("MouseButtonUp", "UnfadeUI");
+    SubscribeToEvent("ChangeLanguage", "HandleChangeLanguage");
 }
 
 void ResizeUI()
@@ -707,6 +708,7 @@ Menu@ CreateMenuItem(const String&in title, MENU_CALLBACK@ callback = null, int
     menu.AddChild(menuText);
     menuText.style = "EditorMenuText";
     menuText.text = title;
+    menuText.autoLocalizable = true;
 
     if (addToQuickMenu)
         AddQuickMenuItem(callback, quickMenuText.empty ? title : quickMenuText);
@@ -796,12 +798,31 @@ Window@ CreatePopup(Menu@ baseMenu)
 Menu@ CreateMenu(const String&in title)
 {
     Menu@ menu = CreateMenuItem(title);
-    menu.SetFixedWidth(menu.width);
+    Text@ text = menu.children[0];
+    menu.maxWidth = text.width + 20;
     CreatePopup(menu);
 
     return menu;
 }
 
+void HandleChangeLanguage(StringHash eventType, VariantMap& eventData)
+{
+    Array<UIElement@> children = uiMenuBar.GetChildren();
+
+    for (uint i = 0; i < children.length - 2; ++i) // last 2 elements is not menu
+    {
+        // dirty hack: force recalc text size
+        children[i].maxWidth = 1000;
+        Text@ text = children[i].children[0];
+        text.minWidth = 0;
+        text.maxWidth = 1;
+        text.ApplyAttributes();
+        children[i].maxWidth = text.width + 20;
+    }
+
+    RebuildResourceDatabase();
+}
+
 Text@ CreateAccelKeyText(int accelKey, int accelQual)
 {
     Text@ accelKeyText = Text();

+ 14 - 0
bin/Data/StringsDe.json

@@ -0,0 +1,14 @@
+{
+	"lang":{
+		"de":"Sprache: Deutsch"
+	},
+	"quit":{
+		"de":"Verlassen"
+	},
+	"Press this button":{
+		"de":"Drücken Sie diese Taste"
+	},
+	"title":{
+		"de":"Titel"
+	}
+}

+ 18 - 0
bin/Data/StringsEnRu.json

@@ -0,0 +1,18 @@
+{
+	"lang":{
+		"en":"Language: English",
+		"ru":"Язык: Русский"
+	},
+	"quit":{
+		"en":"Quit",
+		"ru":"Выйти"
+	},
+	"Press this button":{
+		"en":"Press this button",
+		"ru":"Нажмите эту кнопку"
+	},
+	"title":{
+		"en":"Title",
+		"ru":"Заголовок"
+	}
+}

+ 1 - 0
bin/Data/UI/EditorResourceBrowser.xml

@@ -21,6 +21,7 @@
 			<attribute name="Bottom Left Color" value="0.85 0.85 0.85 1" />
 			<attribute name="Bottom Right Color" value="0.85 0.85 0.85 1" />
 			<attribute name="Text" value="Resource Browser" />
+			<attribute name="Auto Localizable" value="true" />
 		</element>
 		<element type="LineEdit">
 			<attribute name="Name" value="Search" />

+ 2 - 0
bin/EditorRu.bat

@@ -0,0 +1,2 @@
+Editor.bat -language ru -w -s
+