Bladeren bron

* Show how to translate resource strings

michael 6 jaren geleden
bovenliggende
commit
a99f117940

+ 13 - 0
demo/translate/dutch.json

@@ -0,0 +1,13 @@
+{
+  "program" : {
+    "URLTitle":"Vertaling met resourcestrings - URL API"
+  },
+  "mystrings": {
+      "Button":"Vertaal deze pagina",
+      "Header":"Vertaling met resourcestrings",
+      "Paragraph":"Deze tekst wordt vertaald.",
+      "TranslateDirect":"De directe API wordt gebruikt voor dit voorbeeld.",
+      "TranslateJSON":"Een JSON object wordt gebruikt voor dit voorbeeld.",
+      "TranslateURL":"Een URL wordt gebruikt voor dit voorbeeld."
+  }
+}

+ 18 - 0
demo/translate/mystrings.pp

@@ -0,0 +1,18 @@
+unit mystrings;
+
+{$mode objfpc}
+
+interface
+
+Resourcestring
+  Header = 'Translation using resource strings';
+  Paragraph = 'This text will be translated.';
+  TranslateDirect = 'The direct API is used for this example.';
+  TranslateJSON = 'A JSON object is used for this example.';
+  TranslateURL = 'A URL is used for this example.';
+  Button = 'Translate this page';
+
+implementation
+
+end.
+

+ 259 - 0
demo/translate/rstranslate.pp

@@ -0,0 +1,259 @@
+{
+    This file is part of the Pas2JS run time library.
+    Copyright (c) 2019 by Michael Van Canneyt
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    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.
+
+ **********************************************************************}
+unit rstranslate;
+
+{$mode objfpc}
+
+interface
+
+uses
+  SysUtils, JS, web;
+
+Type
+
+  { TResourceTranslator }
+  TLoadFailEvent = Reference to Procedure (Sender : TObject; aCode : Integer; aError : String);
+  TOnTranslatedEvent = Reference to Procedure (Sender : TObject; aURL : String);
+
+  TResourceTranslator = Class
+  Private
+    FOnLoadFail: TLoadFailEvent;
+    FOnURLTranslated: TOnTranslatedEvent;
+    class var FInstance : TResourceTranslator;
+    function GetUnitResources(const aUnit: string): TJSOBject;
+  Protected
+    procedure ResetTranslation(aUnitResources: TJSObject; AString: String); virtual;
+    procedure Translate(aUnitResources: TJSObject; AString, aTranslation: String); virtual;
+  Public
+    Class Function Instance : TResourceTranslator;
+    Procedure Translate(Const aUnit,aString,aTranslation : String); overload;
+    Procedure Translate(Const aTranslations : TJSOBject); overload;
+    Procedure ResetTranslation(Const aUnit : String; const aString : String = ''); overload;
+    procedure Translate(const aURL: string; aOnTranslated : TOnTranslatedEvent = Nil); overload;
+    Property OnLoadFail : TLoadFailEvent Read FOnLoadFail Write FOnLoadFail;
+    Property OnURLTranslated : TOnTranslatedEvent Read FOnURLTranslated Write FOnURLTranslated;
+  end;
+
+Function ResourceTranslator : TResourceTranslator;
+Procedure Translate(Const aURL : String; aOnTranslated : TOnTranslatedEvent = Nil);
+Procedure Translate(Const aUnit,aString,aTranslation : String);
+Procedure Translate(Const aTranslations : TJSOBject);
+Procedure ResetTranslation(Const aUnit : String; Const aString : String = '');
+
+implementation
+
+Const
+  SCurrent = 'current';
+
+var
+  Pas : TJSObject; external name 'pas';
+
+{ TResourceTranslator }
+
+
+procedure TResourceTranslator.Translate(aUnitResources: TJSObject; AString, aTranslation: String);
+
+Var
+  res: JSValue;
+
+begin
+  res:=aUnitResources[aString];
+  if Assigned(Res) then
+    TJSOBject(Res)[SCurrent]:=aTranslation;
+end;
+
+procedure TResourceTranslator.ResetTranslation(aUnitResources: TJSObject; AString: String);
+
+Var
+  res: JSValue;
+
+begin
+  res:=aUnitResources[aString];
+  if Assigned(Res) then
+    TJSOBject(Res)[SCurrent]:=undefined;
+end;
+
+
+function TResourceTranslator.GetUnitResources(const aUnit: string): TJSOBject;
+
+var
+  jsMod,res : JSValue;
+
+begin
+  Result:=Nil;
+  jsMod:=Pas[aUnit];
+  if assigned(jsMod) then
+    begin
+    res:=(TJSObject(jsMod)['$resourcestrings']);
+    if Assigned(res) then
+      Result:=TJSObject(res);
+    end;
+end;
+
+procedure TResourceTranslator.Translate(const aTranslations: TJSOBject);
+
+var
+  aUnitName,aStringName : String;
+  aUnit,Res : TJSObject;
+  V : JSValue;
+
+begin
+  for aUnitName in TJSObject.getOwnPropertyNames(aTranslations) do
+    if IsValidIdent(aUnitName,True) and isObject(aTranslations[aUnitName]) then
+      begin
+      aUnit:=TJSObject(aTranslations[aUnitName]);
+      Res:=GetUnitResources(aUnitName);
+      if Assigned(Res) then
+        For aStringName in TJSObject.getOwnPropertyNames(aUnit) do
+          begin
+          V:=aUnit[aStringName];
+          if IsString(V) then
+            Translate(Res,aStringName,String(V));
+          end;
+      end;
+end;
+
+procedure TResourceTranslator.Translate(const aUnit, aString, aTranslation: String);
+
+Var
+  jsmod : TJSObject;
+
+begin
+  jsMod:=GetUnitResources(aUnit);
+  if Assigned(jsMod) then
+    Translate(jsMod, aString,aTranslation);
+end;
+
+procedure TResourceTranslator.ResetTranslation(const aUnit: String; const aString: String);
+
+Var
+  jsmod : TJSObject;
+  S : String;
+
+begin
+  jsMod:=GetUnitResources(aUnit);
+  if Assigned(jsMod) then
+    if (aString<>'') then
+      ResetTranslation(jsMod, aString)
+    else
+      for S in TJSOBject.getOwnPropertyNames(TJSObject(jsMod)) do
+        ResetTranslation(jsMod, S);
+end;
+
+Type
+
+  { TURLTranslator }
+
+  TURLTranslator = Class
+  Private
+    FTranslator : TResourceTranslator;
+    FURL : String;
+    FXHR : TJSXMLHttpRequest;
+    FOnTranslated : TOnTranslatedEvent;
+    procedure DoStateChange;
+  Public
+    Constructor Create(aUrl : String; ATranslator : TResourceTranslator;aOnTranslated : TOnTranslatedEvent);
+    Procedure Translate;
+  end;
+
+{ TURLTranslator }
+
+procedure TURLTranslator.DoStateChange;
+
+Var
+  O : TJSObject;
+
+begin
+  if (FXHR.ReadyState=TJSXMLHttpRequest.DONE) then
+    if ((FXHR.Status div 100)=2) then
+      begin
+      try
+        O:=TJSJSON.parseObject(FXHR.responseText);
+        FTranslator.Translate(O);
+        if Assigned(FOnTranslated) then
+          FOnTranslated(FTranslator,FURL);
+        if Assigned(FTranslator.OnURLTranslated) then
+          FTranslator.OnURLTranslated(FTranslator,FURL);
+      except
+        if Assigned(FTranslator.OnLoadFail) then
+          FTranslator.OnLoadFail(FTranslator,0,'Invalid JSON')
+      end
+      end
+    else
+      if Assigned(FTranslator.OnLoadFail) then
+        FTranslator.OnLoadFail(FTranslator,FXHR.Status,FXHR.StatusText);
+end;
+
+constructor TURLTranslator.Create(aUrl: String; ATranslator: TResourceTranslator;aOnTranslated : TOnTranslatedEvent);
+begin
+  FURL:=aURL;
+  FTranslator:=ATranslator;
+  FOnTranslated:=aOnTranslated;
+end;
+
+procedure TURLTranslator.Translate;
+begin
+  FXHR:=TJSXMLHttpRequest.new;
+  FXHR.open('GET',fURL);
+  FXHR.onreadystatechange:=@DoStateChange;
+  FXHR.responseType:='text';
+  FXHR.send;
+end;
+
+procedure TResourceTranslator.Translate(const aURL : string;aOnTranslated : TOnTranslatedEvent = Nil);
+
+begin
+  With TURLTranslator.Create(aURL,Self,aOnTranslated) do
+    Translate;
+end;
+
+class function TResourceTranslator.Instance: TResourceTranslator;
+begin
+  if FInstance=Nil then
+    FInstance:=TResourceTranslator.Create;
+  Result:=FInstance;
+end;
+
+{ ---------------------------------------------------------------------
+  Procedural access
+  ---------------------------------------------------------------------}
+
+Procedure Translate(Const aURL : String; aOnTranslated : TOnTranslatedEvent = Nil);
+
+begin
+  TResourceTranslator.Instance.Translate(aURL,aOnTranslated);
+end;
+
+Procedure Translate(Const aTranslations : TJSOBject);
+
+begin
+  TResourceTranslator.Instance.Translate(aTranslations);
+end;
+
+procedure Translate(const aUnit, aString, aTranslation: String);
+begin
+  TResourceTranslator.Instance.Translate(aUnit,aString,aTranslation);
+end;
+
+Procedure ResetTranslation(Const aUnit : String; Const aString : String = '');
+begin
+  TResourceTranslator.Instance.ResetTranslation(aUnit,Astring);
+end;
+
+Function ResourceTranslator : TResourceTranslator;
+begin
+  Result:=TResourceTranslator.Instance;
+end;
+
+end.
+

+ 17 - 0
demo/translate/translate_basic.html

@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Translation using resource strings - Basic API</title>
+  <script src="translate_basic.js"></script>
+</head>
+<body>
+  <h2 id="translate-header"></h2>
+  <p id="translate-text"></p>
+  <button id="btn-translate" class="btn btn-primary"></button>
+  <script>
+  rtl.run();
+  </script>
+</body>
+</html>

+ 95 - 0
demo/translate/translate_basic.lpi

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+        <Runnable Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="translate_basic"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="4">
+      <Item0 Name="MaintainHTML" Value="1"/>
+      <Item1 Name="PasJSHTMLFile" Value="project1.html"/>
+      <Item2 Name="PasJSPort" Value="0"/>
+      <Item3 Name="PasJSWebBrowserProject" Value="1"/>
+    </CustomData>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+      <Modes Count="0"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="translate_basic.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="translate_basic.html"/>
+        <IsPartOfProject Value="True"/>
+        <CustomData Count="1">
+          <Item0 Name="PasJSIsProjectHTMLFile" Value="1"/>
+        </CustomData>
+      </Unit>
+      <Unit>
+        <Filename Value="mystrings.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target FileExt=".js">
+      <Filename Value="translate_basic"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="js"/>
+    </SearchPaths>
+    <Parsing>
+      <SyntaxOptions>
+        <AllowLabel Value="False"/>
+        <CPPInline Value="False"/>
+        <UseAnsiStrings Value="False"/>
+      </SyntaxOptions>
+    </Parsing>
+    <CodeGeneration>
+      <TargetOS Value="browser"/>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <GenerateDebugInfo Value="False"/>
+        <UseLineInfoUnit Value="False"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-Jeutf-8 -Jirtl.js -Jc -Jminclude"/>
+      <CompilerPath Value="$(pas2js)"/>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 91 - 0
demo/translate/translate_basic.lpr

@@ -0,0 +1,91 @@
+program translate_basic;
+
+{$mode objfpc}
+
+uses
+  JS, Web, mystrings, rstranslate;
+
+Resourcestring
+  BasicTitle = 'Translation using resource strings - Basic API';
+
+Type
+
+  { TTranslateApp }
+
+  TTranslateApp = Class
+    FHeader : TJSHTMLELement;
+    FPar : TJSHTMLELement;
+    FButton : TJSHTMLButtonELement;
+    FIsDutch : Boolean;
+    Constructor Create;
+    Function GetEl(const aName : string) : TJSHTMLELement;
+    Procedure Bind;
+    Procedure SetTexts;
+  private
+    function DoTranslation(aEvent: TJSMouseEvent): boolean;
+    procedure TranslateStrings;
+  end;
+
+{ TTranslateApp }
+
+constructor TTranslateApp.Create;
+begin
+  Bind;
+end;
+
+function TTranslateApp.GetEl(const aName: string): TJSHTMLELement;
+begin
+  Result:=TJSHTMLELement(Document.getElementById(aName));
+end;
+
+procedure TTranslateApp.Bind;
+begin
+  FHeader:=GetEl('translate-header');
+  FPar:=GetEl('translate-text');
+  FButton:=TJSHTMLButtonELement(GetEl('btn-translate'));
+  FButton.onclick:=@DoTranslation;
+end;
+
+procedure TTranslateApp.SetTexts;
+begin
+  FHeader.InnerHTML:=Header;
+  FButton.InnerHTML:=Button;
+  FPar.InnerHtml:=Paragraph+' '+TranslateDirect;
+  document.title:=BasicTitle;
+//  window. title:=BasicTitle;
+end;
+
+procedure TTranslateApp.TranslateStrings;
+
+begin
+  FIsDutch:=Not FIsDutch;
+  if FIsDutch then
+    begin
+    Translate('program','BasicTitle','Vertaling met resourcestrings - directe API');
+    Translate('mystrings','Button','Vertaal deze pagina');
+    Translate('mystrings','Header','Vertaling met resourcestrings');
+    Translate('mystrings','Paragraph','Deze tekst wordt vertaald.');
+    Translate('mystrings','TranslateDirect','De directe API wordt gebruikt voor dit voorbeeld.');
+    Translate('mystrings','TranslateJSON','Een JSON object wordt gebruikt voor dit voorbeeld.');
+    Translate('mystrings','TranslateURL','Een URL wordt gebruikt voor dit voorbeeld.');
+    end
+  else
+    begin
+    // Single string of a module
+    ResetTranslation('program','BasicTitle');
+    // All strings in a module
+    ResetTranslation('mystrings');
+    end;
+end;
+
+function TTranslateApp.DoTranslation(aEvent: TJSMouseEvent): boolean;
+begin
+  TranslateStrings;
+  SetTexts;
+  Result:=True;
+end;
+
+begin
+  With TTranslateApp.Create do
+    SetTexts;
+end.

+ 17 - 0
demo/translate/translate_object.html

@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Translation using resource strings - Object API</title>
+  <script src="translate_object.js"></script>
+</head>
+<body>
+  <h2 id="translate-header"></h2>
+  <p id="translate-text"></p>
+  <button id="btn-translate" class="btn btn-primary"></button>
+  <script>
+  rtl.run();
+  </script>
+</body>
+</html>

+ 95 - 0
demo/translate/translate_object.lpi

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+        <Runnable Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="translate_object"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="4">
+      <Item0 Name="MaintainHTML" Value="1"/>
+      <Item1 Name="PasJSHTMLFile" Value="translate_object.html"/>
+      <Item2 Name="PasJSPort" Value="0"/>
+      <Item3 Name="PasJSWebBrowserProject" Value="1"/>
+    </CustomData>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+      <Modes Count="0"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="translate_object.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="translate_object.html"/>
+        <IsPartOfProject Value="True"/>
+        <CustomData Count="1">
+          <Item0 Name="PasJSIsProjectHTMLFile" Value="1"/>
+        </CustomData>
+      </Unit>
+      <Unit>
+        <Filename Value="mystrings.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target FileExt=".js">
+      <Filename Value="translate_object"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="js"/>
+    </SearchPaths>
+    <Parsing>
+      <SyntaxOptions>
+        <AllowLabel Value="False"/>
+        <CPPInline Value="False"/>
+        <UseAnsiStrings Value="False"/>
+      </SyntaxOptions>
+    </Parsing>
+    <CodeGeneration>
+      <TargetOS Value="browser"/>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <GenerateDebugInfo Value="False"/>
+        <UseLineInfoUnit Value="False"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-Jeutf-8 -Jirtl.js -Jc -Jminclude"/>
+      <CompilerPath Value="$(pas2js)"/>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 97 - 0
demo/translate/translate_object.lpr

@@ -0,0 +1,97 @@
+program translate_object;
+
+{$mode objfpc}
+
+uses
+  JS, Web, mystrings, rstranslate;
+
+Resourcestring
+  ObjectTitle = 'Translation using resource strings - Object API';
+
+Type
+
+  { TTranslateApp }
+
+  TTranslateApp = Class
+    FDutch : TJSObject;
+    FHeader : TJSHTMLELement;
+    FPar : TJSHTMLELement;
+    FButton : TJSHTMLButtonELement;
+    FIsDutch : Boolean;
+    Constructor Create;
+    Function GetEl(const aName : string) : TJSHTMLELement;
+    Procedure Bind;
+    Procedure SetTexts;
+  private
+    function DoTranslation(aEvent: TJSMouseEvent): boolean;
+    procedure TranslateStrings;
+  end;
+
+{ TTranslateApp }
+
+constructor TTranslateApp.Create;
+begin
+  Bind;
+  FDutch:=New([
+    'program',new([
+      'BasicTitle','Vertaling met resourcestrings - directe API'
+    ]),
+    'mystrings', new([
+      'Button','Vertaal deze pagina',
+      'Header','Vertaling met resourcestrings',
+      'Paragraph','Deze tekst wordt vertaald.',
+      'TranslateDirect','De directe API wordt gebruikt voor dit voorbeeld.',
+      'TranslateJSON','Een JSON object wordt gebruikt voor dit voorbeeld.',
+      'TranslateURL','Een URL wordt gebruikt voor dit voorbeeld.'
+    ])
+  ]);
+end;
+
+function TTranslateApp.GetEl(const aName: string): TJSHTMLELement;
+begin
+  Result:=TJSHTMLELement(Document.getElementById(aName));
+end;
+
+procedure TTranslateApp.Bind;
+begin
+  FHeader:=GetEl('translate-header');
+  FPar:=GetEl('translate-text');
+  FButton:=TJSHTMLButtonELement(GetEl('btn-translate'));
+  FButton.onclick:=@DoTranslation;
+end;
+
+procedure TTranslateApp.SetTexts;
+begin
+  FHeader.InnerHTML:=Header;
+  FButton.InnerHTML:=Button;
+  FPar.InnerHtml:=Paragraph+' '+TranslateJSON;
+  document.title:=ObjectTitle;
+end;
+
+
+procedure TTranslateApp.TranslateStrings;
+
+begin
+  FIsDutch:=Not FIsDutch;
+  if FIsDutch then
+    Translate(FDutch)
+  else
+    begin
+    // Single string of a module
+    ResetTranslation('program','BasicTitle');
+    // All strings in a module
+    ResetTranslation('mystrings');
+    end;
+end;
+
+function TTranslateApp.DoTranslation(aEvent: TJSMouseEvent): boolean;
+begin
+  TranslateStrings;
+  SetTexts;
+  Result:=True;
+end;
+
+begin
+  With TTranslateApp.Create do
+    SetTexts;
+end.

+ 17 - 0
demo/translate/translate_url.html

@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Translation using resource strings - URL API</title>
+  <script src="translate_url.js"></script>
+</head>
+<body>
+  <h2 id="translate-header"></h2>
+  <p id="translate-text"></p>
+  <button id="btn-translate" class="btn btn-primary"></button>
+  <script>
+  rtl.run();
+  </script>
+</body>
+</html>

+ 95 - 0
demo/translate/translate_url.lpi

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+        <Runnable Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="translate_url"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="4">
+      <Item0 Name="MaintainHTML" Value="1"/>
+      <Item1 Name="PasJSHTMLFile" Value="translate_url.html"/>
+      <Item2 Name="PasJSPort" Value="0"/>
+      <Item3 Name="PasJSWebBrowserProject" Value="1"/>
+    </CustomData>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+      <Modes Count="0"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="translate_url.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="translate_url.html"/>
+        <IsPartOfProject Value="True"/>
+        <CustomData Count="1">
+          <Item0 Name="PasJSIsProjectHTMLFile" Value="1"/>
+        </CustomData>
+      </Unit>
+      <Unit>
+        <Filename Value="mystrings.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target FileExt=".js">
+      <Filename Value="translate_url"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="js"/>
+    </SearchPaths>
+    <Parsing>
+      <SyntaxOptions>
+        <AllowLabel Value="False"/>
+        <CPPInline Value="False"/>
+        <UseAnsiStrings Value="False"/>
+      </SyntaxOptions>
+    </Parsing>
+    <CodeGeneration>
+      <TargetOS Value="browser"/>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <GenerateDebugInfo Value="False"/>
+        <UseLineInfoUnit Value="False"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-Jeutf-8 -Jirtl.js -Jc -Jminclude"/>
+      <CompilerPath Value="$(pas2js)"/>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 89 - 0
demo/translate/translate_url.lpr

@@ -0,0 +1,89 @@
+program translate_url;
+
+{$mode objfpc}
+
+uses
+  JS, Web, rstranslate, mystrings;
+
+Resourcestring
+  URLTitle = 'Translation using resource strings - URL API';
+
+Type
+
+  { TTranslateApp }
+
+  TTranslateApp = Class
+    FHeader : TJSHTMLELement;
+    FPar : TJSHTMLELement;
+    FButton : TJSHTMLButtonELement;
+    FIsDutch : Boolean;
+    Constructor Create;
+    Function GetEl(const aName : string) : TJSHTMLELement;
+    Procedure Bind;
+    Procedure SetTexts;
+  private
+    procedure DoneTranslation(Sender: TObject; aURL: String);
+    function DoTranslation(aEvent: TJSMouseEvent): boolean;
+    procedure TranslateStrings;
+  end;
+
+{ TTranslateApp }
+
+constructor TTranslateApp.Create;
+begin
+  Bind;
+end;
+
+function TTranslateApp.GetEl(const aName: string): TJSHTMLELement;
+begin
+  Result:=TJSHTMLELement(Document.getElementById(aName));
+end;
+
+procedure TTranslateApp.Bind;
+begin
+  FHeader:=GetEl('translate-header');
+  FPar:=GetEl('translate-text');
+  FButton:=TJSHTMLButtonELement(GetEl('btn-translate'));
+  FButton.onclick:=@DoTranslation;
+end;
+
+procedure TTranslateApp.SetTexts;
+begin
+  FHeader.InnerHTML:=Header;
+  FButton.InnerHTML:=Button;
+  FPar.InnerHtml:=Paragraph+' '+TranslateURL;
+  document.title:=URLTitle;
+end;
+
+procedure TTranslateApp.DoneTranslation(Sender : TObject; aURL : String);
+
+begin
+  SetTexts;
+end;
+
+procedure TTranslateApp.TranslateStrings;
+
+begin
+  FIsDutch:=Not FIsDutch;
+  if FIsDutch then
+    Translate('dutch.json',@DoneTranslation)
+  else
+    begin
+    // Single string of a module
+    ResetTranslation('program','URLTitle');
+    // All strings in a module
+    ResetTranslation('mystrings');
+    end;
+end;
+
+function TTranslateApp.DoTranslation(aEvent: TJSMouseEvent): boolean;
+begin
+  TranslateStrings;
+  SetTexts;
+  Result:=True;
+end;
+
+begin
+  With TTranslateApp.Create do
+    SetTexts;
+end.

+ 247 - 0
packages/rtl/rstranslate.pp

@@ -0,0 +1,247 @@
+unit rstranslate;
+
+{$mode objfpc}
+
+interface
+
+uses
+  SysUtils, JS, web;
+
+Type
+
+  { TResourceTranslator }
+  TLoadFailEvent = Reference to Procedure (Sender : TObject; aCode : Integer; aError : String);
+  TOnTranslatedEvent = Reference to Procedure (Sender : TObject; aURL : String);
+
+  TResourceTranslator = Class
+  Private
+    FOnLoadFail: TLoadFailEvent;
+    FOnURLTranslated: TOnTranslatedEvent;
+    class var FInstance : TResourceTranslator;
+    function GetUnitResources(const aUnit: string): TJSOBject;
+  Protected
+    procedure ResetTranslation(aUnitResources: TJSObject; AString: String); virtual;
+    procedure Translate(aUnitResources: TJSObject; AString, aTranslation: String); virtual;
+  Public
+    Class Function Instance : TResourceTranslator;
+    Procedure Translate(Const aUnit,aString,aTranslation : String); overload;
+    Procedure Translate(Const aTranslations : TJSOBject); overload;
+    Procedure ResetTranslation(Const aUnit : String; const aString : String = ''); overload;
+    procedure Translate(const aURL: string; aOnTranslated : TOnTranslatedEvent = Nil); overload;
+    Property OnLoadFail : TLoadFailEvent Read FOnLoadFail Write FOnLoadFail;
+    Property OnURLTranslated : TOnTranslatedEvent Read FOnURLTranslated Write FOnURLTranslated;
+  end;
+
+Function ResourceTranslator : TResourceTranslator;
+Procedure Translate(Const aURL : String; aOnTranslated : TOnTranslatedEvent = Nil);
+Procedure Translate(Const aUnit,aString,aTranslation : String);
+Procedure Translate(Const aTranslations : TJSOBject);
+Procedure ResetTranslation(Const aUnit : String; Const aString : String = '');
+
+implementation
+
+Const
+  SCurrent = 'current';
+
+var
+  Pas : TJSObject; external name 'pas';
+
+{ TResourceTranslator }
+
+
+procedure TResourceTranslator.Translate(aUnitResources: TJSObject; AString, aTranslation: String);
+
+Var
+  res: JSValue;
+
+begin
+  res:=aUnitResources[aString];
+  if Assigned(Res) then
+    TJSOBject(Res)[SCurrent]:=aTranslation;
+end;
+
+procedure TResourceTranslator.ResetTranslation(aUnitResources: TJSObject; AString: String);
+
+Var
+  res: JSValue;
+
+begin
+  res:=aUnitResources[aString];
+  if Assigned(Res) then
+    TJSOBject(Res)[SCurrent]:=undefined;
+end;
+
+
+function TResourceTranslator.GetUnitResources(const aUnit: string): TJSOBject;
+
+var
+  jsMod,res : JSValue;
+
+begin
+  Result:=Nil;
+  jsMod:=Pas[aUnit];
+  if assigned(jsMod) then
+    begin
+    res:=(TJSObject(jsMod)['$resourcestrings']);
+    if Assigned(res) then
+      Result:=TJSObject(res);
+    end;
+end;
+
+procedure TResourceTranslator.Translate(const aTranslations: TJSOBject);
+
+var
+  aUnitName,aStringName : String;
+  aUnit,Res : TJSObject;
+  V : JSValue;
+
+begin
+  for aUnitName in TJSObject.getOwnPropertyNames(aTranslations) do
+    if IsValidIdent(aUnitName,True) and isObject(aTranslations[aUnitName]) then
+      begin
+      aUnit:=TJSObject(aTranslations[aUnitName]);
+      Res:=GetUnitResources(aUnitName);
+      if Assigned(Res) then
+        For aStringName in TJSObject.getOwnPropertyNames(aUnit) do
+          begin
+          V:=aUnit[aStringName];
+          if IsString(V) then
+            Translate(Res,aStringName,String(V));
+          end;
+      end;
+end;
+
+procedure TResourceTranslator.Translate(const aUnit, aString, aTranslation: String);
+
+Var
+  jsmod : TJSObject;
+
+begin
+  jsMod:=GetUnitResources(aUnit);
+  if Assigned(jsMod) then
+    Translate(jsMod, aString,aTranslation);
+end;
+
+procedure TResourceTranslator.ResetTranslation(const aUnit: String; const aString: String);
+
+Var
+  jsmod : TJSObject;
+  S : String;
+
+begin
+  jsMod:=GetUnitResources(aUnit);
+  if Assigned(jsMod) then
+    if (aString<>'') then
+      ResetTranslation(jsMod, aString)
+    else
+      for S in TJSOBject.getOwnPropertyNames(TJSObject(jsMod)) do
+        ResetTranslation(jsMod, S);
+end;
+
+Type
+
+  { TURLTranslator }
+
+  TURLTranslator = Class
+  Private
+    FTranslator : TResourceTranslator;
+    FURL : String;
+    FXHR : TJSXMLHttpRequest;
+    FOnTranslated : TOnTranslatedEvent;
+    procedure DoStateChange;
+  Public
+    Constructor Create(aUrl : String; ATranslator : TResourceTranslator;aOnTranslated : TOnTranslatedEvent);
+    Procedure Translate;
+  end;
+
+{ TURLTranslator }
+
+procedure TURLTranslator.DoStateChange;
+
+Var
+  O : TJSObject;
+
+begin
+  if (FXHR.ReadyState=TJSXMLHttpRequest.DONE) then
+    if ((FXHR.Status div 100)=2) then
+      begin
+      try
+        O:=TJSJSON.parseObject(FXHR.responseText);
+        FTranslator.Translate(O);
+        if Assigned(FOnTranslated) then
+          FOnTranslated(FTranslator,FURL);
+        if Assigned(FTranslator.OnURLTranslated) then
+          FTranslator.OnURLTranslated(FTranslator,FURL);
+      except
+        if Assigned(FTranslator.OnLoadFail) then
+          FTranslator.OnLoadFail(FTranslator,0,'Invalid JSON')
+      end
+      end
+    else
+      if Assigned(FTranslator.OnLoadFail) then
+        FTranslator.OnLoadFail(FTranslator,FXHR.Status,FXHR.StatusText);
+end;
+
+constructor TURLTranslator.Create(aUrl: String; ATranslator: TResourceTranslator;aOnTranslated : TOnTranslatedEvent);
+begin
+  FURL:=aURL;
+  FTranslator:=ATranslator;
+  FOnTranslated:=aOnTranslated;
+end;
+
+procedure TURLTranslator.Translate;
+begin
+  FXHR:=TJSXMLHttpRequest.new;
+  FXHR.open('GET',fURL);
+  FXHR.onreadystatechange:=@DoStateChange;
+  FXHR.responseType:='text';
+  FXHR.send;
+end;
+
+procedure TResourceTranslator.Translate(const aURL : string;aOnTranslated : TOnTranslatedEvent = Nil);
+
+begin
+  With TURLTranslator.Create(aURL,Self,aOnTranslated) do
+    Translate;
+end;
+
+class function TResourceTranslator.Instance: TResourceTranslator;
+begin
+  if FInstance=Nil then
+    FInstance:=TResourceTranslator.Create;
+  Result:=FInstance;
+end;
+
+{ ---------------------------------------------------------------------
+  Procedural access
+  ---------------------------------------------------------------------}
+
+Procedure Translate(Const aURL : String; aOnTranslated : TOnTranslatedEvent = Nil);
+
+begin
+  TResourceTranslator.Instance.Translate(aURL,aOnTranslated);
+end;
+
+Procedure Translate(Const aTranslations : TJSOBject);
+
+begin
+  TResourceTranslator.Instance.Translate(aTranslations);
+end;
+
+procedure Translate(const aUnit, aString, aTranslation: String);
+begin
+  TResourceTranslator.Instance.Translate(aUnit,aString,aTranslation);
+end;
+
+Procedure ResetTranslation(Const aUnit : String; Const aString : String = '');
+begin
+  TResourceTranslator.Instance.ResetTranslation(aUnit,Astring);
+end;
+
+Function ResourceTranslator : TResourceTranslator;
+begin
+  Result:=TResourceTranslator.Instance;
+end;
+
+end.
+