<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="SF_L10N" script:language="StarBasic" script:moduleType="normal">REM =======================================================================================================================
REM ===			The ScriptForge library and its associated libraries are part of the LibreOffice project.				===
REM ===					Full documentation is available on https://help.libreoffice.org/								===
REM =======================================================================================================================

Option Compatible
Option ClassModule
&apos;Option Private Module

Option Explicit

&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;
&apos;&apos;&apos;	L10N (aka SF_L10N)
&apos;&apos;&apos;	====
&apos;&apos;&apos;		Implementation of a Basic class for providing a number of services
&apos;&apos;&apos;		related to the translation of user interfaces into a huge number of languages
&apos;&apos;&apos;		with a minimal impact on the program code itself
&apos;&apos;&apos;
&apos;&apos;&apos;		The design choices of this module are based on so-called PO-files
&apos;&apos;&apos;		PO-files (portable object files) have long been promoted in the free software industry
&apos;&apos;&apos;		as a mean of providing multilingual UIs. This is accomplished through the use of human-readable
&apos;&apos;&apos;		text files with a well defined structure that specifies, for any given language,
&apos;&apos;&apos;		the source language string and the localized string
&apos;&apos;&apos;
&apos;&apos;&apos;		To read more about the PO format and its ecosystem of associated toolsets:
&apos;&apos;&apos;			https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files
&apos;&apos;&apos;		and, IMHO, a very good tutorial:
&apos;&apos;&apos;			http://pology.nedohodnik.net/doc/user/en_US/ch-about.html
&apos;&apos;&apos;
&apos;&apos;&apos;		The main advantage of the PO format is the complete dissociation between the two
&apos;&apos;&apos;		very different profiles, i.e. the programmer and the translator(s).
&apos;&apos;&apos;		Being independent text files, one per language to support, the programmer may give away
&apos;&apos;&apos;		pristine PO template files (known as POT-files) for a translator to process.
&apos;&apos;&apos;
&apos;&apos;&apos;		This class implements mainly 4 mechanisms:
&apos;&apos;&apos;			1. AddText:	for the programmer to build a set of words or sentences
&apos;&apos;&apos;						meant for being translated later
&apos;&apos;&apos;			2. AddTextsFromDialog:	to automatically execute AddText() on each fixed text of a dialog
&apos;&apos;&apos;			3. ExportToPOTFile:	All the above texts are exported into a pristine POT-file
&apos;&apos;&apos;			4. GetText:	At runtime get the text in the user language
&apos;&apos;&apos;		Note that the first two are optional: POT and PO-files may be built with a simple text editor
&apos;&apos;&apos;
&apos;&apos;&apos;		Several instances of the L10N class may coexist
&apos;		The constraint however is that each instance should find its PO-files
&apos;&apos;&apos;		in a separate directory
&apos;&apos;&apos;		PO-files must be named with the targeted locale: f.i. &quot;en-US.po&quot; or &quot;fr-BE.po&quot;
&apos;&apos;&apos;
&apos;&apos;&apos;		Service invocation syntax
&apos;&apos;&apos;			CreateScriptService(&quot;L10N&quot;[, FolderName[, Locale]])
&apos;&apos;&apos;				FolderName: the folder containing the PO-files (in SF_FileSystem.FileNaming notation)
&apos;&apos;&apos;				Locale: in the form la-CO (language-COUNTRY)
&apos;&apos;&apos;				Encoding: The character set that should be used (default = UTF-8)
&apos;&apos;&apos;					Use one of the Names listed in https://www.iana.org/assignments/character-sets/character-sets.xhtml
&apos;&apos;&apos;				Locale2: fallback Locale to select if Locale po file does not exist (typically &quot;en-US&quot;)
&apos;&apos;&apos;				Encoding2: Encoding of the 2nd Locale file
&apos;&apos;&apos;		Service invocation examples:
&apos;&apos;&apos;			Dim myPO As Variant
&apos;&apos;&apos;			myPO = CreateScriptService(&quot;L10N&quot;)	&apos;	AddText, AddTextsFromDialog and ExportToPOTFile are allowed
&apos;&apos;&apos;			myPO = CreateScriptService(&quot;L10N&quot;, &quot;C:\myPOFiles\&quot;, &quot;fr-BE&quot;)
&apos;&apos;&apos;				&apos;All functionalities are available
&apos;&apos;&apos;
&apos;&apos;&apos;		Detailed user documentation:
&apos;&apos;&apos;			https://help.libreoffice.org/latest/en-US/text/sbasic/shared/03/sf_l10n.html?DbPAR=BASIC
&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;

REM =============================================================== PRIVATE TYPES

&apos;&apos;&apos;	The recognized elements of an entry in a PO file are (other elements are ignored) :
&apos;&apos;&apos;		#. Extracted comments (given by the programmer to the translator)
&apos;&apos;&apos;		#, flag (the kde-format flag when the string contains tokens)
&apos;&apos;&apos;		msgctxt Context (to store an acronym associated with the message, this is a distortion of the norm)
&apos;&apos;&apos;		msgid untranslated-string
&apos;&apos;&apos;		msgstr translated-string
&apos;&apos;&apos;	NB: plural forms are not supported

Type POEntry
	Comment			As String
	Flag			As String
	Context			As String
	MsgId			As String
	MsgStr			As String
End Type

REM ================================================================== EXCEPTIONS

Const DUPLICATEKEYERROR		=	&quot;DUPLICATEKEYERROR&quot;

REM ============================================================= PRIVATE MEMBERS

Private [Me]				As Object
Private [_Parent]			As Object
Private ObjectType			As String		&apos; Must be &quot;L10N&quot;
Private ServiceName			As String
Private _POFolder			As String		&apos; PO files container
Private _Locale				As String		&apos; la-CO
Private _POFile				As String		&apos; PO file in URL format
Private _Encoding			As String		&apos; Used to open the PO file, default = UTF-8
Private _Dictionary			As Object		&apos; SF_Dictionary

REM ===================================================== CONSTRUCTOR/DESTRUCTOR

REM -----------------------------------------------------------------------------
Private Sub Class_Initialize()
	Set [Me] = Nothing
	Set [_Parent] = Nothing
	ObjectType = &quot;L10N&quot;
	ServiceName = &quot;ScriptForge.L10N&quot;
	_POFolder = &quot;&quot;
	_Locale = &quot;&quot;
	_POFile = &quot;&quot;
	Set _Dictionary = Nothing
End Sub		&apos;	ScriptForge.SF_L10N Constructor

REM -----------------------------------------------------------------------------
Private Sub Class_Terminate()
	
	If Not IsNull(_Dictionary) Then Set _Dictionary = _Dictionary.Dispose()
	Call Class_Initialize()
End Sub		&apos;	ScriptForge.SF_L10N Destructor

REM -----------------------------------------------------------------------------
Public Function Dispose() As Variant
	Call Class_Terminate()
	Set Dispose = Nothing
End Function	&apos;	ScriptForge.SF_L10N Explicit Destructor

REM ================================================================== PROPERTIES

REM -----------------------------------------------------------------------------
Property Get Folder() As String
&apos;&apos;&apos;	Returns the FolderName containing the PO-files expressed as given by the current FileNaming
&apos;&apos;&apos;	property of the SF_FileSystem service. Default = URL format
&apos;&apos;&apos;	May be empty
&apos;&apos;&apos;	Example:
&apos;&apos;&apos;		myPO.Folder

	Folder = _PropertyGet(&quot;Folder&quot;)

End Property	&apos;	ScriptForge.SF_L10N.Folder

REM -----------------------------------------------------------------------------
Property Get Languages() As Variant
&apos;&apos;&apos;	Returns a zero-based array listing all the BaseNames of the PO-files found in Folder, 
&apos;&apos;&apos;	Example:
&apos;&apos;&apos;		myPO.Languages

	Languages = _PropertyGet(&quot;Languages&quot;)

End Property	&apos;	ScriptForge.SF_L10N.Languages

REM -----------------------------------------------------------------------------
Property Get Locale() As String
&apos;&apos;&apos;	Returns the currently active language-COUNTRY combination. May be empty
&apos;&apos;&apos;	Example:
&apos;&apos;&apos;		myPO.Locale

	Locale = _PropertyGet(&quot;Locale&quot;)

End Property	&apos;	ScriptForge.SF_L10N.Locale

REM ===================================================================== METHODS

REM -----------------------------------------------------------------------------
Public Function AddText(Optional ByVal Context As Variant _
								, Optional ByVal MsgId As Variant _
								, Optional ByVal Comment As Variant _
								, Optional ByVal MsgStr As Variant _
								) As Boolean
&apos;&apos;&apos; Add a new entry in the list of localizable text strings
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Context: when not empty, the key to retrieve the translated string via GetText. Default = &quot;&quot;
&apos;&apos;&apos;		MsgId: the untranslated string, i.e. the text appearing in the program code. Must not be empty
&apos;&apos;&apos;			The key to retrieve the translated string via GetText when Context is empty
&apos;&apos;&apos;			May contain placeholders (%1 ... %9) for dynamic arguments to be inserted in the text at run-time
&apos;&apos;&apos;			If the string spans multiple lines, insert escape sequences (\n) where relevant
&apos;&apos;&apos;		Comment: the so-called &quot;extracted-comments&quot; intended to inform/help translators
&apos;&apos;&apos;			If the string spans multiple lines, insert escape sequences (\n) where relevant
&apos;&apos;&apos;		MsgStr: (internal use only) the translated string
&apos;&apos;&apos;			If the string spans multiple lines, insert escape sequences (\n) where relevant
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		True if successful
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		DUPLICATEKEYERROR: such a key exists already
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		myPO.AddText(, &quot;This is a text to be included in a POT file&quot;)

Dim bAdd As Boolean				&apos;   Output buffer
Dim sKey As String				&apos;	The key part of the new entry in the dictionary
Dim vItem As POEntry			&apos;	The item part of the new entry in the dictionary
Const cstPipe = &quot;|&quot;				&apos;	Pipe forbidden in MsgId&apos;s
Const cstThisSub = &quot;L10N.AddText&quot;
Const cstSubArgs = &quot;[Context=&quot;&quot;&quot;&quot;], MsgId, [Comment=&quot;&quot;&quot;&quot;]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	bAdd = False

Check:
	If IsMissing(Context) Or IsMissing(Context) Then Context = &quot;&quot;
	If IsMissing(Comment) Or IsMissing(Comment) Then Comment = &quot;&quot;
	If IsMissing(MsgStr) Or IsMissing(MsgStr) Then MsgStr = &quot;&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(Context, &quot;Context&quot;, V_STRING) Then GoTo Finally
		If Not SF_Utils._Validate(MsgId, &quot;MsgId&quot;, V_STRING) Then GoTo Finally
		If Not SF_Utils._Validate(Comment, &quot;Comment&quot;, V_STRING) Then GoTo Finally
		If Not SF_Utils._Validate(MsgStr, &quot;MsgStr&quot;, V_STRING) Then GoTo Finally
	End If
	If Len(MsgId) = 0 Then GoTo Finally

Try:
	If Len(Context) &gt; 0 Then sKey = Context Else sKey = MsgId
	If _Dictionary.Exists(sKey) Then GoTo CatchDuplicate

	With vItem
		.Comment = Comment
		If InStr(MsgId, &quot;%&quot;) &gt; 0 Then .Flag = &quot;kde-format&quot; Else .Flag = &quot;&quot;
		.Context = Replace(Context, cstPipe, &quot; &quot;)
		.MsgId = Replace(MsgId, cstPipe, &quot; &quot;)
		.MsgStr = MsgStr
	End With
	_Dictionary.Add(sKey, vItem)
	bAdd = True

Finally:
	AddText = bAdd
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchDuplicate:
	SF_Exception.RaiseFatal(DUPLICATEKEYERROR, Iif(Len(Context) &gt; 0, &quot;Context&quot;, &quot;MsgId&quot;), sKey)
	GoTo Finally
End Function    &apos;   ScriptForge.SF_L10N.AddText

REM -----------------------------------------------------------------------------
Public Function AddTextsFromDialog(Optional ByRef Dialog As Variant) As Boolean
&apos;&apos;&apos; Add all fixed text strings of a dialog to the list of localizable text strings
&apos;&apos;&apos;	Added texts are:
&apos;&apos;&apos;		- the title of the dialog
&apos;&apos;&apos;		- the caption associated with next control types: Button, CheckBox, FixedLine, FixedText, GroupBox and RadioButton
&apos;&apos;&apos;		- the content of list- and comboboxes
&apos;&apos;&apos;		- the tip- or helptext displayed when the mouse is hovering the control
&apos;&apos;&apos;	The current method has method SFDialogs.SF_Dialog.GetTextsFromL10N as counterpart
&apos;&apos;&apos;	The targeted dialog must not be open when the current method is run
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Dialog: a SFDialogs.Dialog service instance
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		True when successful
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		Dim myDialog As Object
&apos;&apos;&apos;		Set myDialog = CreateScriptService(&quot;SFDialogs.Dialog&quot;, &quot;GlobalScope&quot;, &quot;XrayTool&quot;, &quot;DlgXray&quot;)
&apos;&apos;&apos;		myPO.AddTextsFromDialog(myDialog)

Dim bAdd As Boolean					&apos;	Return value
Dim vControls As Variant			&apos;	Array of control names
Dim sControl As String				&apos;	A single control name
Dim oControl As Object				&apos;	SFDialogs.DialogControl
Dim sText As String					&apos;	The text to insert in the dictionary
Dim sDialogComment As String		&apos;	The prefix in the comment to insert in the dictionary for the dialog
Dim sControlComment As String		&apos;	The prefix in the comment to insert in the dictionary for a control
Dim vSource As Variant				&apos;	RowSource property of dialog control as an array
Dim i As Long

Const cstThisSub = &quot;L10N.AddTextsFromDialog&quot;
Const cstSubArgs = &quot;Dialog&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	bAdd = False

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(Dialog, &quot;Dialog&quot;, V_OBJECT, , , &quot;DIALOG&quot;) Then GoTo Finally
	End If

Try:
	With Dialog
		&apos;	Store the title of the dialog
		sDialogComment = &quot;Dialog =&gt; &quot; &amp; ._Container &amp; &quot; : &quot; &amp; ._Library &amp; &quot; : &quot; &amp; ._Name &amp; &quot; : &quot;
		stext = .Caption
		If Len(sText) &gt; 0 Then
			If Not _ReplaceText(&quot;&quot;, sText, sDialogComment &amp; &quot;Caption&quot;) Then GoTo Catch
		End If
		&apos;	Scan all controls
		vControls = .Controls()
		For Each sControl In vControls
			Set oControl = .Controls(sControl)
			sControlComment = sDialogComment &amp; sControl &amp; &quot;.&quot;
			With oControl
				&apos;	Extract fixed texts
				sText = .Caption
				If Len(sText) &gt; 0 Then
					If Not _ReplaceText(&quot;&quot;, sText, sControlComment &amp; &quot;Caption&quot;) Then GoTo Catch
				End If
				vSource = .RowSource	&apos;	List and comboboxes only
				If IsArray(vSource) Then
					For i = 0 To UBound(vSource)
						If Len(vSource(i)) &gt; 0 Then
							If Not _ReplaceText(&quot;&quot;, vSource(i), sControlComment &amp; &quot;RowSource[&quot; &amp; i &amp; &quot;]&quot;) Then GoTo Catch
						End If
					Next i
				End If
				sText = .TipText
				If Len(sText) &gt; 0 Then
					If Not _ReplaceText(&quot;&quot;, sText, sControlComment &amp; &quot;TipText&quot;) Then GoTo Catch
				End If
			End With
		Next sControl
	End With

	bAdd = True

Finally:
	AddTextsFromDialog = bAdd
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function    &apos;   ScriptForge.SF_L10N.AddTextsFromDialog

REM -----------------------------------------------------------------------------
Public Function ExportToPOTFile(Optional ByVal FileName As Variant _
									, Optional ByVal Header As Variant _
									, Optional ByVal Encoding As Variant _
									) As Boolean
&apos;&apos;&apos; Export a set of untranslated strings as a POT file
&apos;&apos;&apos;	The set of strings has been built either by a succession of AddText() methods
&apos;&apos;&apos;		or by a successful invocation of the L10N service with the FolderName argument
&apos;&apos;&apos;	The generated file should pass successfully the &quot;msgfmt --check &apos;the pofile&apos;&quot; GNU command
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		FileName: the complete file name to export to. If it exists, is overwritten without warning
&apos;&apos;&apos;		Header: Comments that will appear on top of the generated file. Do not include any leading &quot;#&quot;
&apos;&apos;&apos;			If the string spans multiple lines, insert escape sequences (\n) where relevant
&apos;&apos;&apos;			A standard header will be added anyway
&apos;&apos;&apos;		Encoding: The character set that should be used
&apos;&apos;&apos;				Use one of the Names listed in https://www.iana.org/assignments/character-sets/character-sets.xhtml
&apos;&apos;&apos;				Note that LibreOffice probably does not implement all existing sets
&apos;&apos;&apos;				Default = UTF-8
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		True if successful
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		myPO.ExportToPOTFile(&quot;myFile.pot&quot;, Header := &quot;Top comment\nSecond line of top comment&quot;)

Dim bExport As Boolean				&apos;   Return value
Dim oFile As Object					&apos;	Generated file handler
Dim vLines As Variant				&apos;	Wrapped lines
Dim sLine As String					&apos;	A single line
Dim vItems As Variant				&apos;	Array of dictionary items
Dim vItem As Variant				&apos;	POEntry type
Const cstSharp = &quot;#  &quot;, cstSharpDot = &quot;#. &quot;, cstFlag = &quot;#, kde-format&quot;
Const cstTabSize = 4
Const cstWrap = 70
Const cstThisSub = &quot;L10N.ExportToPOTFile&quot;
Const cstSubArgs = &quot;FileName, [Header=&quot;&quot;&quot;&quot;], [Encoding=&quot;&quot;UTF-8&quot;&quot;&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	bExport = False

Check:
	If IsMissing(Header) Or IsEmpty(Header) Then Header = &quot;&quot;
	If IsMissing(Encoding) Or IsEmpty(Encoding) Then Encoding = &quot;UTF-8&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateFile(FileName, &quot;FileName&quot;) Then GoTo Finally
		If Not SF_Utils._Validate(Header, &quot;Header&quot;, V_STRING) Then GoTo Finally
		If Not SF_Utils._Validate(Encoding, &quot;Encoding&quot;, V_STRING) Then GoTo Finally
	End If

Try:
	Set oFile = SF_FileSystem.CreateTextFile(FileName, Overwrite := True, Encoding := Encoding)
	If Not IsNull(oFile) Then
		With oFile
			&apos;	Standard header
			.WriteLine(cstSharp)
			.WriteLine(cstSharp &amp; &quot;This pristine POT file has been generated by LibreOffice/ScriptForge&quot;)
			.WriteLine(cstSharp &amp; &quot;Full documentation is available on https://help.libreoffice.org/&quot;)
			&apos;	User header
			If Len(Header) &gt; 0 Then
				.WriteLine(cstSharp)
				vLines = SF_String.Wrap(Header, cstWrap, cstTabSize)
				For Each sLine In vLines
					.WriteLine(cstSharp &amp; Replace(sLine, SF_String.sfLF, &quot;&quot;))
				Next sLine
			End If
			&apos;	Standard header
			.WriteLine(cstSharp)
			.WriteLine(&quot;msgid &quot;&quot;&quot;&quot;&quot;)
			.WriteLine(&quot;msgstr &quot;&quot;&quot;&quot;&quot;)
			.WriteLine(SF_String.Quote(&quot;Project-Id-Version: PACKAGE VERSION\n&quot;))
			.WriteLine(SF_String.Quote(&quot;Report-Msgid-Bugs-To: &quot; _
					&amp; &quot;https://bugs.libreoffice.org/enter_bug.cgi?product=LibreOffice&amp;bug_status=UNCONFIRMED&amp;component=UI\n&quot;))
			.WriteLine(SF_String.Quote(&quot;POT-Creation-Date: &quot; &amp; SF_STring.Represent(Now()) &amp; &quot;\n&quot;))
			.WriteLine(SF_String.Quote(&quot;PO-Revision-Date: YYYY-MM-DD HH:MM:SS\n&quot;))
			.WriteLine(SF_String.Quote(&quot;Last-Translator: FULL NAME &lt;EMAIL@ADDRESS&gt;\n&quot;))
			.WriteLine(SF_String.Quote(&quot;Language-Team: LANGUAGE &lt;EMAIL@ADDRESS&gt;\n&quot;))
			.WriteLine(SF_String.Quote(&quot;Language: en_US\n&quot;))
			.WriteLine(SF_String.Quote(&quot;MIME-Version: 1.0\n&quot;))
			.WriteLine(SF_String.Quote(&quot;Content-Type: text/plain; charset=&quot; &amp; Encoding &amp; &quot;\n&quot;))
			.WriteLine(SF_String.Quote(&quot;Content-Transfer-Encoding: 8bit\n&quot;))
			.WriteLine(SF_String.Quote(&quot;Plural-Forms: nplurals=2; plural=n &gt; 1;\n&quot;))
			.WriteLine(SF_String.Quote(&quot;X-Generator: LibreOffice - ScriptForge\n&quot;))
			.WriteLine(SF_String.Quote(&quot;X-Accelerator-Marker: ~\n&quot;))
			&apos;	Individual translatable strings
			vItems = _Dictionary.Items()
			For Each vItem in vItems
				.WriteBlankLines(1)
				&apos;	Comments
				vLines = Split(vItem.Comment, &quot;\n&quot;)
				For Each sLine In vLines
					.WriteLine(cstSharpDot &amp; SF_String.ExpandTabs(SF_String.Unescape(sLine), cstTabSize))
				Next sLine
				&apos;	Flag
				If InStr(vItem.MsgId, &quot;%&quot;) &gt; 0 Then .WriteLine(cstFlag)
				&apos;	Context
				If Len(vItem.Context) &gt; 0 Then
					.WriteLine(&quot;msgctxt &quot; &amp; SF_String.Quote(vItem.Context))
				End If
				&apos;	MsgId
				vLines = SF_String.Wrap(vItem.MsgId, cstWrap, cstTabSize)
				If UBound(vLines) = 0 Then
					.WriteLine(&quot;msgid  &quot; &amp; SF_String.Quote(SF_String.Escape(vLines(0))))
				Else
					.WriteLine(&quot;msgid  &quot;&quot;&quot;&quot;&quot;)
					For Each sLine in vLines
						.WriteLine(SF_String.Quote(SF_String.Escape(sLine)))
					Next sLine
				End If
				&apos;	MsgStr
				.WriteLine(&quot;msgstr &quot;&quot;&quot;&quot;&quot;)
			Next vItem
			.CloseFile()
		End With
	End If
	bExport = True

Finally:
	If Not IsNull(oFile) Then Set oFile = oFile.Dispose()
	ExportToPOTFile = bExport
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function    &apos;   ScriptForge.SF_L10N.ExportToPOTFile

REM -----------------------------------------------------------------------------
Public Function GetProperty(Optional ByVal PropertyName As Variant) As Variant
&apos;&apos;&apos;	Return the actual value of the given property
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		PropertyName: the name of the property as a string
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		The actual value of the property
&apos;&apos;&apos;		If the property does not exist, returns Null
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARGUMENTERROR		The property does not exist
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		myL10N.GetProperty(&quot;MyProperty&quot;)

Const cstThisSub = &quot;L10N.GetProperty&quot;
Const cstSubArgs = &quot;&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	GetProperty = Null

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(PropertyName, &quot;PropertyName&quot;, V_STRING, Properties()) Then GoTo Catch
	End If

Try:
	GetProperty = _PropertyGet(PropertyName)

Finally:
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_L10N.GetProperty

REM -----------------------------------------------------------------------------
Public Function GetText(Optional ByVal MsgId As Variant _
							, ParamArray pvArgs As Variant _
							) As String
&apos;&apos;&apos; Get the translated string corresponding with the given argument
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		MsgId: the identifier of the string or the untranslated string
&apos;&apos;&apos;			Either	- the untranslated text (MsgId)
&apos;&apos;&apos;					- the reference to the untranslated text (Context)
&apos;&apos;&apos;					- both (Context|MsgId) : the pipe character is essential
&apos;&apos;&apos;		pvArgs(): a list of arguments present as %1, %2, ... in the (un)translated string)
&apos;&apos;&apos;			to be substituted in the returned string
&apos;&apos;&apos;			Any type is admitted but only strings, numbers or dates are relevant
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		The translated string
&apos;&apos;&apos;		If not found the MsgId string or the Context string
&apos;&apos;&apos;		Anyway the substitution is done
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		myPO.GetText(&quot;This is a text to be included in a POT file&quot;)
&apos;&apos;&apos;			&apos;	Ceci est un text à inclure dans un fichier POT

Dim sText As String				&apos;   Output buffer
Dim sContext As String			&apos;	Context part of argument
Dim sMsgId As String			&apos;	MsgId part of argument
Dim vItem As POEntry			&apos;	Entry in the dictionary
Dim vMsgId As Variant			&apos;	MsgId split on pipe
Dim sKey As String				&apos;	Key of dictionary
Dim sPercent As String			&apos;	%1, %2, ... placeholders
Dim i As Long
Const cstPipe = &quot;|&quot;
Const cstThisSub = &quot;L10N.GetText&quot;
Const cstSubArgs = &quot;MsgId, [Arg0, Arg1, ...]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	sText = &quot;&quot;

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(MsgId, &quot;MsgId&quot;, V_STRING) Then GoTo Finally
	End If
	If Len(Trim(MsgId)) = 0 Then GoTo Finally
	sText = MsgId

Try:
	&apos;	Find and load entry from dictionary
	If Left(MsgId, 1) = cstPipe then MsgId = Mid(MsgId, 2)
	vMsgId = Split(MsgId, cstPipe)
	sKey = vMsgId(0)
	If Not _Dictionary.Exists(sKey) Then	&apos;	Not found
		If UBound(vMsgId) = 0 Then sText = vMsgId(0) Else sText = Mid(MsgId, InStr(MsgId, cstPipe) + 1)
	Else
		vItem = _Dictionary.Item(sKey)
		If Len(vItem.MsgStr) &gt; 0 Then sText = vItem.MsgStr Else sText = vItem.MsgId
	End If

	&apos;	Substitute %i placeholders
	For i = UBound(pvArgs) To 0	Step -1	&apos;	Go downwards to not have a limit in number of args
		sPercent = &quot;%&quot; &amp; (i + 1)
		sText = Replace(sText, sPercent, SF_String.Represent(pvArgs(i)))
	Next i

Finally:
	GetText = sText
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function    &apos;   ScriptForge.SF_L10N.GetText

REM - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Public Function _(Optional ByVal MsgId As Variant _
						, ParamArray pvArgs As Variant _
						) As String
&apos;&apos;&apos; Get the translated string corresponding with the given argument
&apos;&apos;&apos;	Alias of GetText() - See above
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		myPO._(&quot;This is a text to be included in a POT file&quot;)
&apos;&apos;&apos;			&apos;	Ceci est un text à inclure dans un fichier POT

Dim sText As String				&apos;   Output buffer
Dim sPercent As String			&apos;	%1, %2, ... placeholders
Dim i As Long
Const cstPipe = &quot;|&quot;
Const cstThisSub = &quot;L10N._&quot;
Const cstSubArgs = &quot;MsgId, [Arg0, Arg1, ...]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	sText = &quot;&quot;

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(MsgId, &quot;MsgId&quot;, V_STRING) Then GoTo Finally
	End If
	If Len(Trim(MsgId)) = 0 Then GoTo Finally

Try:
	&apos;	Find and load entry from dictionary
	sText = GetText(MsgId)

	&apos;	Substitute %i placeholders - done here, not in GetText(), because # of arguments is undefined
	For i = 0 To UBound(pvArgs)
		sPercent = &quot;%&quot; &amp; (i + 1)
		sText = Replace(sText, sPercent, SF_String.Represent(pvArgs(i)))
	Next i

Finally:
	_ = sText
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function    &apos;   ScriptForge.SF_L10N._

REM -----------------------------------------------------------------------------
Public Function Methods() As Variant
&apos;&apos;&apos;	Return the list of public methods of the L10N service as an array

	Methods = Array( _
					&quot;AddText&quot; _
					, &quot;ExportToPOTFile&quot; _
					, &quot;GetText&quot; _
					, &quot;AddTextsFromDialog&quot; _
					, &quot;_&quot; _
					)

End Function	&apos;	ScriptForge.SF_L10N.Methods

REM -----------------------------------------------------------------------------
Public Function Properties() As Variant
&apos;&apos;&apos;	Return the list or properties of the Timer class as an array

	Properties = Array( _
					&quot;Folder&quot; _
					, &quot;Languages&quot; _
					, &quot;Locale&quot; _
					)

End Function	&apos;	ScriptForge.SF_L10N.Properties

REM -----------------------------------------------------------------------------
Public Function SetProperty(Optional ByVal PropertyName As Variant _
								, Optional ByRef Value As Variant _
								) As Boolean
&apos;&apos;&apos;	Set a new value to the given property
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		PropertyName: the name of the property as a string
&apos;&apos;&apos;		Value: its new value
&apos;&apos;&apos;	Exceptions
&apos;&apos;&apos;		ARGUMENTERROR		The property does not exist

Const cstThisSub = &quot;L10N.SetProperty&quot;
Const cstSubArgs = &quot;PropertyName, Value&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	SetProperty = False

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(PropertyName, &quot;PropertyName&quot;, V_STRING, Properties()) Then GoTo Catch
	End If

Try:
	Select Case UCase(PropertyName)
		Case Else
	End Select

Finally:
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_L10N.SetProperty

REM =========================================================== PRIVATE FUNCTIONS

REM -----------------------------------------------------------------------------
Public Sub _Initialize(ByVal psPOFile As String _
							, ByVal Encoding As String _
							)
&apos;&apos;&apos;	Completes initialization of the current instance requested from CreateScriptService()
&apos;&apos;&apos;	Load the POFile in the dictionary, otherwise leave the dictionary empty
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		psPOFile: the file to load the translated strings from
&apos;&apos;&apos;		Encoding: The character set that should be used. Default = UTF-8

Dim oFile As Object			&apos;	PO file handler
Dim sContext As String		&apos;	Collected context string
Dim sMsgId As String		&apos;	Collected untranslated string
Dim sComment As String		&apos;	Collected comment string
Dim sMsgStr As String		&apos;	Collected translated string
Dim sLine As String			&apos;	Last line read
Dim iContinue As Integer	&apos;	0 = None, 1 = MsgId, 2 = MsgStr
Const cstMsgId = 1, cstMsgStr = 2

Try:
	&apos;	Initialize dictionary anyway with case-sensitive comparison of keys
	Set _Dictionary = SF_Services.CreateScriptService(&quot;Dictionary&quot;, True)
	Set _Dictionary.[_Parent] = [Me]

	&apos;	Load PO file
	If Len(psPOFile) &gt; 0 Then
		With SF_FileSystem
			_POFolder = ._ConvertToUrl(.GetParentFolderName(psPOFile))
			_Locale = .GetBaseName(psPOFile)
			_POFile = ._ConvertToUrl(psPOFile)
		End With
		&apos;	Load PO file
		Set oFile = SF_FileSystem.OpenTextFile(psPOFile, IOMode := SF_FileSystem.ForReading, Encoding := Encoding)
		If Not IsNull(oFile) Then
			With oFile
				&apos;	The PO file is presumed valid =&gt; syntax check is not very strict
				sContext = &quot;&quot;	:	sMsgId = &quot;&quot;	:	sComment = &quot;&quot;	:	sMsgStr = &quot;&quot;
				Do While Not .AtEndOfStream
					sLine = Trim(.ReadLine())
					&apos;	Trivial examination of line header
					Select Case True
						Case sLine = &quot;&quot;
							If Len(sMsgId) &gt; 0 Then AddText(sContext, sMsgId, sComment, sMsgStr)
							sContext = &quot;&quot;	:	sMsgId = &quot;&quot;	:	sComment = &quot;&quot;	:	sMsgStr = &quot;&quot;
							iContinue = 0
						Case Left(sLine, 3) = &quot;#. &quot;
							sComment = sComment &amp; Iif(Len(sComment) &gt; 0, &quot;\n&quot;, &quot;&quot;) &amp; Trim(Mid(sLine, 4))
							iContinue = 0
						Case Left(sLine, 8) = &quot;msgctxt &quot;
							sContext = SF_String.Unquote(Trim(Mid(sLine, 9)))
							iContinue = 0
						Case Left(sLine, 6) = &quot;msgid &quot;
							sMsgId = SF_String.Unquote(Trim(Mid(sLine, 7)))
							iContinue = cstMsgId
						Case Left(sLine, 7) = &quot;msgstr &quot;
							sMsgStr = sMsgStr &amp; SF_String.Unquote(Trim(Mid(sLine, 8)))
							iContinue = cstMsgStr
						Case Left(sLine, 1) = &quot;&quot;&quot;&quot;
							If iContinue = cstMsgId Then
								sMsgId = sMsgId &amp; SF_String.Unquote(sLine)
							ElseIf iContinue = cstMsgStr Then
								sMsgStr = sMsgStr &amp; SF_String.Unquote(sLine)
							Else
								iContinue = 0
							End If
						Case Else	&apos;	Skip line
							iContinue = 0
					End Select
				Loop
				&apos;	Be sure to store the last entry
				If Len(sMsgId) &gt; 0 Then AddText(sContext, sMsgId, sComment, sMsgStr)
				.CloseFile()
				Set oFile = .Dispose()
			End With
		End If
	Else
		_POFolder = &quot;&quot;
		_Locale = &quot;&quot;
		_POFile = &quot;&quot;
	End If

Finally:
	Exit Sub
End Sub			&apos;	ScriptForge.SF_L10N._Initialize

REM -----------------------------------------------------------------------------
Private Function _PropertyGet(Optional ByVal psProperty As String)
&apos;&apos;&apos;	Return the value of the named property
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		psProperty: the name of the property

Dim vFiles As Variant		&apos;	Array of PO-files
Dim i As Long
Dim cstThisSub As String
Dim cstSubArgs As String

	cstThisSub = &quot;SF_L10N.get&quot; &amp; psProperty
	cstSubArgs = &quot;&quot;
	SF_Utils._EnterFunction(cstThisSub, cstSubArgs)

	With SF_FileSystem
		Select Case psProperty
			Case &quot;Folder&quot;
				If Len(_POFolder) &gt; 0 Then _PropertyGet = ._ConvertFromUrl(_POFolder) Else _PropertyGet = &quot;&quot;
			Case &quot;Languages&quot;
				If Len(_POFolder) &gt; 0 Then
					vFiles = .Files(._ConvertFromUrl(_POFolder), &quot;*.po&quot;)
					For i = 0 To UBound(vFiles)
						vFiles(i) = SF_FileSystem.GetBaseName(vFiles(i))
					Next i
				Else
					vFiles = Array()
				End If
				_PropertyGet = vFiles
			Case &quot;Locale&quot;
				_PropertyGet = _Locale
			Case Else
				_PropertyGet = Null
		End Select
	End With

Finally:
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
End Function	&apos;	ScriptForge.SF_L10N._PropertyGet

REM -----------------------------------------------------------------------------
Private Function _ReplaceText(ByVal psContext As String _
								, ByVal psMsgId As String _
								, ByVal psComment As String _
								) As Boolean
&apos;&apos;&apos;	When the entry in the dictionary does not yet exist, equivalent to AddText
&apos;&apos;&apos;	When it exists already, extend the existing comment with the psComment argument
&apos;&apos;&apos;	Used from AddTextsFromDialog to manage identical strings without raising errors,
&apos;&apos;&apos;	e.g. when multiple dialogs have the same &quot;Close&quot; button

Dim bAdd As Boolean					&apos;	Return value
Dim sKey As String					&apos;	The key part of an entry in the dictionary
Dim vItem As POEntry				&apos;	The item part of the new entry in the dictionary

Try:
	bAdd = False
	If Len(psContext) &gt; 0 Then sKey = psContext Else sKey = psMsgId
	If _Dictionary.Exists(sKey) Then
		&apos;	Load the entry, adapt comment and rewrite
		vItem = _Dictionary.Item(sKey)
		If Len(vItem.Comment) = 0 Then vItem.Comment = psComment Else vItem.Comment = vItem.Comment &amp; &quot;\n&quot; &amp; psComment
		bAdd = _Dictionary.ReplaceItem(sKey, vItem)
	Else
		&apos;	Add a new entry as usual
		bAdd = AddText(psContext, psMsgId, psComment)
	End If

Finally:
	_ReplaceText = bAdd
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_L10N._ReplaceText

REM -----------------------------------------------------------------------------
Private Function _Repr() As String
&apos;&apos;&apos;	Convert the L10N instance to a readable string, typically for debugging purposes (DebugPrint ...)
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;	Return:
&apos;&apos;&apos;		&quot;[L10N]: PO file&quot;

	_Repr = &quot;[L10N]: &quot; &amp; _POFile

End Function	&apos;	ScriptForge.SF_L10N._Repr

REM ============================================ END OF SCRIPTFORGE.SF_L10N
</script:module>