--[[----------------------------------------------------------------------------

WFZString.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2011 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

--------------------------------------------------------------------------------

Much of this code has been adapted from Lightroom's AgZString.lua.

------------------------------------------------------------------------------]]


--============================================================================--
--[[!
	\class WFZString

	\brief This namespace provides utilities for using the
		Adobe ZString localization mechanism.

	A ZString identifies a unique string in an application's UI, and
	associates it with a localized display string. Each ZString is a
	string that functions as a key-value pair. The key has
	three dollar sign characters followed by an application-defined
	slash-separated path. This key is separated from the localizable
	string value with an equal sign (=). For example:
	
\code
"$$$/ZString/Subscope/Example=This is an example"
\endcode

	ZStrings are "harvested" from code by a tool that generates a ZString file,
	which acts as a translation map. The Zstring file is localized into each
	supported language by altering the value part of each uniquely-identified string,
	and the localized file is then named according to a format that includes the locale code.

	An application checks for a ZString file for the current locale, and uses
	the localized values found in that file to override the default English
	string values that are coded into the program for each key.
	
	To convert a ZString found in the localization file into its localized display
	string, use \c WFZString.fromZString(key):
	
\code
WFZString.fromZString "$$$/ZString/Subscope/Example"
\endcode
		
	This returns the localized string for "This is an example".

	ZStrings in code should consist entirely of low-ASCII characters. The key
	should only contain characters in the set <code>"a-zA-Z0-9/"</code>. The value can
	contain any low-ASCII character.

	\c WFZString.fromZString() supports a mechanism known as hat-substitution, for creating
	dynamic strings that contain other symbols or characters.
	
	For example, pass the hat sequence "^C" to a string that contains "^1" to
	substitute the Copyright symbol at that position in the string.
	The general purpose ^U allows you to encode any arbitrary Unicode character
	given its Unicode code point, in the form ^U+1234.


]]--

--=============================================================================

	-- wichitafoundation.core.os
local KSPathUtils = require 'KSPathUtils'

	-- wichitafoundation.core.utils
local WFArguments = require 'WFArguments'

	-- wichitafoundation.core.string
local WFZStringHashSequences = require 'WFZStringHashSequences'

local WFPlatformInfo = require 'WFPlatformInfo'

--==============================================================================

-- @mark WFZString namespace

local WFZString = {}

--------------------------------------------------------------------------------

-- Use this flag to debg what is and isn't going through WFZString code.
-- Turn off before ship!

WFZString.DEBUG_LOCALIZATION = false
local WFZString_DEBUG_LOCALIZATION = WFZString.DEBUG_LOCALIZATION

--------------------------------------------------------------------------------

local assert = assert
local check = assert( WFArguments.check )

local DEBUG = _G.DEBUG
local debug_print = debug.print

local string_find = assert( string.find )
local string_sub = assert( string.sub )
local string_gsub = assert( string.gsub )
local string_replace = assert( string.replace )
local string_len = assert( string.len )
local string_format = assert( string.format )
local string_char = assert( string.char )
local string_byte = assert( string.byte )

local math_mod = assert( math.mod )
local math_floor = assert( math.floor )

local translationMap

local isClassic = WFPlatformInfo.isClassic()

--=============================================================================
-- Divides and rounds down the result.

local div = function( a, b )
	return math_floor( a / b )
end

--------------------------------------------------------------------------------
--[[
	Encodes a single Unicode character code point as a UTF-8 string.
 	
	\param ch A Unicode code point number (an integer)
	\return The UTF-8 string corresponding to the code point.
]]

local function encodeUTF8Character( --[[number]] ch )

	if ch > 0 and ch <= 127 then
		return string_char( ch )

	elseif ch <= 2047 then
		return string_char( 192 + div( ch, 64 ),
						128 + math_mod( ch, 64 ) )
	
	elseif ch <= 65535 then
		return string_char( 224 + div( ch , 4096 ),
						128 + math_mod( div( ch, 64 ) , 64 ),
						128 + math_mod( ch, 64 ) )
	
	elseif ch <= 2097151 then
		return string_char( 240 + div( ch, 262144 ),
						128 + math_mod( div( ch, 4096 ), 64 ),
						128 + math_mod( div( ch, 64 ), 64 ),
						128 + math_mod( ch, 64 ) )

	elseif ch <= 67108863 then
		return string_char( 248 + div( ch, 16777216 ),
						128 + math_mod( div( ch, 262144 ), 64 ),
						128 + math_mod( div( ch, 4096 ), 64 ),
						128 + math_mod( div( ch, 64 ), 64 ),
						128 + math_mod( ch, 64 ) )

	elseif ch <= 2147483647 then
		return string_char( 252 + div( ch, 1073741824 ),
						128 + math_mod( div( ch, 16777216 ), 64 ),
						128 + math_mod( div( ch, 262144 ), 64 ),
						128 + math_mod( div( ch, 4096 ), 64 ),
						128 + math_mod( div( ch, 64 ), 64 ),
						128 + math_mod( ch, 64 ) )

	end
	
	-- If we get here, it was an invalid input. Return nothing.

end

WFZString.encodeUTF8Character = encodeUTF8Character

--------------------------------------------------------------------------------
--[[
	Given a character for a predefined hat sequence point, return the replacement
	string in UTF-8 encoding.
	
	\param afterChar the character immediately following '^'
	
	\return the string that replaces the predefined hat sequence if the given
		character was valid; else return nil
--]]

local kHatSequences = {
	[ "r" ] =   13,		-- carraige return
	[ "n" ] =   10,		-- line feed
	[ "B" ] = 8226,		-- bullet
	[ "C" ] =  169,		-- copyright symbol
	[ "D" ] =  176,		-- degree sign
	[ "I" ] = 8710,		-- increment
	[ "R" ] =  174,		-- registered sign
	[ "S" ] = 8721,		-- summation
	[ "T" ] = 8482,		-- trademark sign
	[ "!" ] =  172,		-- not sign
	[ "{" ] = 8216,		-- left single quotation mark
	[ "}" ] = 8217,		-- right single quotation mark
	[ "[" ] = 8220,		-- left double quotation mark
	[ "]" ] = 8221,		-- right double quotation mark
	[ "'" ] = 8217,		-- right single quotation mark
	[ "e" ] =  233,		-- latin small letter e with acute
	[ "E" ] =  234,		-- latin small letter e with circumflex
	[ "d" ] =  916,		-- greek capital letter delta
	[ "L" ] =   92,		-- reverse solidus (a.k.a. backslash)
	[ "V" ] =  124,		-- vertical line
	[ "#" ] = 8984,		-- command key
	[ "`" ] =   96,		-- grave accent (`)
	[ "^" ] =   94,		-- circumflex
	[ "." ] = 8230,		-- ellipsis ("..." character)
}

local function findHatEscapeSequence( afterChar )

	local code = kHatSequences[ afterChar ]
	return code and encodeUTF8Character( code )
	
	-- If we get here, it was an invalid escape character. Just return nil.

end

--------------------------------------------------------------------------------
--[[
	Given a single hex digit (as an ASCII character value), return the integer
	value of that digit.
	
	\param hexChar character with hexadecimal number
	
	\return number representing the hexadecimal digit
--]]

local function hexCharToNibble( hexChar )

	if hexChar >= 48 and hexChar <= 57 then
		return hexChar - 48
	elseif hexChar >= 65 and hexChar <= 70 then
		return hexChar - 55
	elseif hexChar >= 97 and hexChar <= 102 then
		return hexChar - 87
	else
		debug_print( "WFZString.hexCharToNibble given a non-hex character: "
					 .. string_char( hexChar ) )
		return 0
	end

end

WFZString.hexCharToNibble = hexCharToNibble

--------------------------------------------------------------------------------
--[[
	Given a four-character hex string, interpret that string as a Unicode
	character code point and return the Unicode code point number.
	
	\param hexChar four-character hex string
	
	\return Unicode code point number
--]]

local function hexStringToUnicodeValue( hex )

	return
		( hexCharToNibble( string_byte( hex, 1 ) ) * 4096 ) +
		( hexCharToNibble( string_byte( hex, 2 ) ) * 256 ) +
		( hexCharToNibble( string_byte( hex, 3 ) ) * 16 ) +
		hexCharToNibble( string_byte( hex, 4 ) )

end

--------------------------------------------------------------------------------
--[[
	This function will replace all valid predefined hat substitutions with the
	appropriate string. Numbered hat sequence points will be ignored here so
	that it can be processed later.
	
	\param str ZString value with hat sequence points
	
	\return string with predefined hat sequences replaced with appropriate string
		
	FIXME [aksherry 2011-04-19] The code is a straight adoption from LR. The
	loop below can probably be optimized with a string.gmatch call, but in the
	interest of saving time, we can optimize later.
--]]

local function stringWithReplacedHatSequences( str )

	str = WFZStringHashSequences.stringWithReplacedHashSequences( str )

	-- Short-circuit if no hat sequences or quotes in this string.

	local hatStart, hatEnd = string_find( str, '^', 1, true )
	local quoteStart = string_find( str, '"', 1, true )
						-- " <-- fix syntax color in Halide on Mac
	if not hatStart and not quoteStart then
		return str
	end

	-- Replace any occurances of backslash-quote with just a quote.
	-- This is so that ZString files can contain quotes more easily for them.

	str = string_replace( str, '\\"', '"' )

	if not hatStart then
		return str		-- Short-circuit the rest if no hats
	end

	-- if any backslashes where removed above, hatStart may have moved

	hatStart, hatEnd = string_find( str, '^', 1, true )

	-- Keep replacing hat sequences until we can't find any more of them.

	while hatStart do

		-- If hat is last character in string, just leave it in place.
		-- Otherwise, figure out what character follows it and make the
		-- appropriate substitution.

		if hatEnd < string_len( str ) then

			local afterChar = string_sub( str, hatStart + 1, hatStart + 1 )
			local replacement = findHatEscapeSequence( afterChar )

			if replacement then

				-- Known hat sequence: Replace string.

				hatEnd = hatStart + 1

			elseif afterChar == 'U' then
			
				-- Unicode escape sequence: Read 4 hex digits, then replace string.

				hatEnd = hatStart + 6
				local hexString = string_sub( str, hatStart + 3, hatStart + 6 )
				replacement = encodeUTF8Character( hexStringToUnicodeValue( hexString ) )

			elseif afterChar >= '0' and afterChar <= '9' then

				-- Numeric hat sequence: Skip it for later processing.

				hatEnd = hatStart + 1

			else

				-- Not a match: Skip the sequence and complain.

				debug_print( 'WFZString.stringWithReplacedHatSequences: Unrecognized hat sequence: ^'
							 .. afterChar .. ' in "' .. tostring( str ) .. '"' )
				hatEnd = hatStart + 1

			end

			if replacement then

				str = string_sub( str, 1, hatStart - 1 ) .. replacement
							.. string_sub( str, hatEnd + 1 )

			end

			-- Look for another hat sequence after this one.

			local searchStart = hatEnd
			if replacement then
				searchStart = hatStart + string_len( replacement )
			end

			hatStart, hatEnd = string_find( str, '^', searchStart, true )

		else
			break
		end

	end

	return str

end

WFZString.stringWithReplacedHatSequences = stringWithReplacedHatSequences

--------------------------------------------------------------------------------
--[[
	Given a string that may contain numbered hat sequence points identified by
	^0 .. ^9, replace the occurrences of the specified hat sequence with the
	specified string.
	
	\param str ZString value
	\param whicHat hat sequence number
	\param replacement replacement string
	
	\return string with hat sequence points replaced
--]]

local function replaceHat( str, whichHat, replacement )

	assert( type( str ) == 'string', "Base string not passed to WFZString.replaceHat" )
	
	if replacement == nil then
		debug_print( string_format( 'WFZString.replaceHat with nil replacement, using empty string instead; str="%s", whichHat="%s"',
					  tostring( str ), tostring( whichHat ) ) )
		replacement = ''
	end

	if not( whichHat >= 0 and whichHat <= 9 ) then
		debug_print( 'WFZString.replaceHat: passed bad whichHat parameter: ' .. whichHat )
		return str
	end
	
	if DEBUG then

		-- We should be able to fix the number case by using our own version of sprintf
		-- when we get around to adding it. [1/29/2007 jsteinme]

		if type( replacement ) == 'number' then

			if replacement % 1 ~= 0 then
				debug_print( string_format( 'WFZString.replaceHat with non-integral number replacement, using tostring() instead; str="%s", whichHat="%s", replacement="%s"',
											tostring( str ), tostring( whichHat ),
											tostring( replacement ) ) )
			end

		elseif type( replacement ) ~= 'string' then

			debug_print( string_format( 'WFZString.replaceHat with non-string replacement, using tostring() instead; str="%s", whichHat="%s", replacement="%s"',
										tostring( str ), tostring( whichHat ),
										tostring( replacement ) ) )

		end

	end

	local pattern = '^' .. whichHat
	local result = string_replace( str, pattern, replacement )
	
	return result

end

WFZString.replaceHat = replaceHat

--------------------------------------------------------------------------------
--[[
	Given a string that may contain named replacement points identified by
	"{key}", replace the occurrences of the specified item with the specified
	string.
	
	\param str ZString value
	\param key name of replacment (without curly braces)
	\param replacement replacement string
	
	\return string with hat sequence points replaced
--]]

local function replacePlaceholder( str, key, replacement )

	assert( type( str ) == 'string', "Base string not passed to WFZString.replacePlaceholder" )
	
	if replacement == nil then
		debug_print( string_format( 'WFZString.replacePlaceholder with nil replacement, using empty string instead; str="%s", key="%s"',
					  tostring( str ), key ) )
		replacement = ''
	end

	if DEBUG then

		-- We should be able to fix the number case by using our own version of sprintf
		-- when we get around to adding it. [1/29/2007 jsteinme]

		if type( replacement ) == 'number' then

			if replacement % 1 ~= 0 then
				debug_print( string_format( 'WFZString.replacePlaceholder with non-integral number replacement, using tostring() instead; str="%s", key="%s", replacement="%s"',
											tostring( str ), tostring( key ),
											tostring( replacement ) ) )
			end

		elseif type( replacement ) ~= 'string' then

			debug_print( string_format( 'WFZString.replacePlaceholder with non-string replacement, using tostring() instead; str="%s", key="%s", replacement="%s"',
										tostring( str ), tostring( key ),
										tostring( replacement ) ) )

		end

	end

	local pattern = '{' .. key .. '}'
	local result, count = string_replace( str, pattern, replacement )
	
	if count == 0 then
		debug_print( string_format( 'WFZString.replacePlaceholder: expected a replacement for %s but had none in "%s"',
									 pattern, tostring( str ) ) )
	end
	
	return result

end

-------------------------------------------------------------------------------
--[[
	Takes a ZString and separates the key from the string. No translation is
	performed on the string. If the input ZString is malformed, the function
	throws an error.

	\param zstr ZString with key and string.
	\param usesFileSyntax (boolean) true if str has a double quote at the
		beginning and end, and a final CR at the end of the string
	
	\return keyStr (string) the key from ZString
	\return str (string) the actual string in ZString
--]]

local function parseZString( zstr, usesFileSyntax, errorDepth )

	-- Note: 'usesFileSyntax' means the string comes in with a quote at the beginning and end,
	-- with a final CR at the end of the string.

	errorDepth = errorDepth or 2

	local equalPos = string_find( zstr, '=' )

	-- Sanity check: does it begin with 3 dollar signs?

	local prefixPos = string_find( zstr, '%$%$%$' )
	local expectedPrefixPos = 1
	if usesFileSyntax then
		expectedPrefixPos = 2
	end
		
	if equalPos == nil or prefixPos ~= expectedPrefixPos then
		return
	end
	
	-- Malformed ZString : Replace \n and \\n with ^n 
	-- In LrC there are many zstr which uses \n and to avoid the error
	-- replacing it with ^n 
	if isClassic then 
		zstr = string.gsub(zstr, "\\n", "^n"):gsub("\n", "^n")
	end
	
	if string_find( zstr, '\n' ) then
		error( 'Malformed ZString ( has \\n. Use ^n instead ): "' .. tostring( zstr ) .. '"',
				   errorDepth )
	end


	if usesFileSyntax then

		-- Ignore the leading quote and the trailing quote and CR.

		local lastQuotePos
		for i = #zstr, 1, -1 do
			if string_sub( zstr, i, i ) == '"' then -- " (to fool the syntax coloring)
				lastQuotePos = i
				break
			end
		end

		if lastQuotePos == nil or lastQuotePos <= equalPos then
			error( 'Malformed ZString (missing trailing quote): "' .. tostring( zstr ) .. '"',
				   errorDepth )
		end

		return string_sub( zstr, 2, equalPos - 1 ),
			   string_sub( zstr, equalPos + 1, lastQuotePos - 1 )

	end
	
	return string_sub( zstr, 1, equalPos - 1 ), string_sub( zstr, equalPos + 1, -1 )
	
end

--------------------------------------------------------------------------------
--[[!
	\brief Creates a local translation map from the contents of a (translated_ZStrings).txt
	  file. The translation map is a table in which each ZString path is the key
	  for its translated string value.
	
	\param text (string) The contents of the (translated_ZStrings).txt file
	\param existingTranslationMap (optional, table) If present, new translations
		are appended to this existing translation map. If not supplied, creates
		a new table.
	
	\return (table) The new or existing translation map table.
--]]

function WFZString.makeTranslationMapFromString( text, existingTranslationMap )

	local result = existingTranslationMap or {}

	for line in string.gmatch( text, "[^\r\n]+" ) do

		-- Allow for 'full line' comments, though not 'end of line' comments.
		if not string.find( line, "^[ \t]*%-%-" ) then
			local key, translated = parseZString( line, true )
			if key then
				result[ key ] = translated
			end
		end

	end
	
	return result
end

--------------------------------------------------------------------------------
--[[!
	\brief Creates a local translation map from a (translated_ZStrings).txt file.
	  The translation map is a table in which each ZString path is the key
	  for its translated string value.
	
	\param fileName (string) The path to the (translated_ZStrings).txt file
	\param existingTranslationMap (optional, table) If present, new translations
		are appended to this existing translation map. If not supplied, creates
		a new table.
	
	\return (table) The new or existing translation map table.
--]]

function WFZString.makeTranslationMap( --[[string]] fileName,
					--[[optional table]] existingTranslationMap )

	check( 'string', '?table' )

	local result = existingTranslationMap or {}
	
	local f = io.open( fileName, 'r' )
	
	if f then

		local success, message = pcall( function() -- ignore errors, but make sure we clean up the open file

			local text = f:read( "*all" )
			
			-- If the file contains a UTF-8 byte-order marker (BOM), skip it.
			
			local b, o, m = string.byte( text, 1, 3 )
			
			if b == 239 and o == 187 and m == 191 then
				text = string_sub( text, 4 )
			end
			
			WFZString.makeTranslationMapFromString( text, result )

		end )

		io.close( f )
		
		if not success then
			error( message )
		end

	end

	return result

end

local customTranslatedStringsFileFetcher
function WFZString.setCustomStringsFileFetcher( f )

	assert( translationMap == nil, 'Must set custom fetcher before any strings are localized' )
	customTranslatedStringsFileFetcher = f

end


--------------------------------------------------------------------------------
--[[
	Find the translation map file at a predetermined location and load it.
--]]

local function makeTranslationMapOnce()
	
	-- Only try to read the translation file once!

	-- This function is called MANY TIMES, so check early and bail out if we've
	-- done this before.
	
	if translationMap == nil then
		
		-- Note: We need to do this lazy call to load the strings file because the AgBundle
		-- mechanism is UNAVAILABLE while this file is being loaded; its too early.
		
		-- [scouten 02/22/06]: I believe AgToolkit (replacement for AgBundle) is
		-- available before this file gets loaded. Don't know who wrote the above
		-- comment, but you could rewrite this code and depend on AgToolkit during
		-- the load sequence now. Since I don't know what was desired, I'm going to
		-- leave it essentially as is.
		
		-- [bruzenak 07/27/06]: This does not necessarily mean AgToolkit functionality
		-- doesn't depend on uninitialized functionality. fileNamed is an example, depending
		-- on AgToolkitLoader when that has not been initialized. Caveat emptor.

		-- [aksherry 2011-09-30]: FIXME It's going to be Mac and iOS specific code for now.
		-- LR has Windows specific code to get translation file.

		local stringFilePath
		
		if type( customTranslatedStringsFileFetcher ) == "function" then
			stringFilePath = customTranslatedStringsFileFetcher()
		else
			--[[
				Clients can optionally provide a testing-only interface to set
				KSPrefs.ks_useMockZstrings. When enabled, this preference will load
				a special translation dictionary in the directory zz_zz. In Revel's
				case, the loc team has prepared this special dictionary to use numeric
				string identifiers in the value so that they can track translation bugs
				more easily.
			]]
			
			stringFilePath = KSPathUtils.getStandardFilePath( 'TranslatedStrings' )
		end

		if stringFilePath ~= nil then
			translationMap = WFZString.makeTranslationMap( stringFilePath )
		else
			-- Use an empty map which will default to English strings.
			translationMap = {}
		end

	end

end

--------------------------------------------------------------------------------

--[[
	Given the English ZString and the translation map file, return the properly
	localized string. The return value of last resort is the original zstr we
	got handed. If the translation fails, return the right hand side of the
	given ZString.
	
	\param zstr (string) default ZString
	\param map (table) translation string map
	
	\return (string) translated string or default string
		
	NOTE: This function is a pared down version of LR's. LR has code to handle
	localization for plugins, something Wichita does not have at the moment.
--]]

local function translateWithMap( zstr, map, errorDepth )

	errorDepth = errorDepth or 2

	local result = zstr
	local key, english = parseZString( zstr, false, errorDepth + 1 )

	if key then
		result = map[ key ]

		if result == nil then

			-- SPAM [scouten 2009-11-19, PR# 2487370]: Q&D hack. Plug-in translation falls back
			-- to Lightroom's built-in strings.

			if map ~= translationMap and map.fallBackToLightroomMap then
				result = translationMap[ key ]
			end

			-- Go with the right hand side of the zstring we got handed.

			if result == nil then
				result = english
			end

		end

	end

	return result

end

WFZString.translateWithMap = translateWithMap

--------------------------------------------------------------------------------
--[[!
	\brief Retrieves the localized display string from a ZString, using the
		application's global translation map.
	
	\param key (string) The ZString key.
	\param ... Optional dynamic-string substitutions. If supplied, evaluates
		the ZString and replaces occurrances of ^1 through ^9 with special characters,
		in the order given. These hat-substitution sequences are defined:

\code
^r			ControlCarriageReturn
^n			ControlLineFeed

^B			Bullet
^C			CopyrightSign
^D			DegreeSign
^I			Increment
^R			RegisteredSign
^S			NArySummation
^T			TradeMarkSign
^!			NotSign

^{			LeftSingleQuotationMark
^}			RightSingleQuotationMark
^[			LeftDoubleQuotationMark
^]			RightDoubleQuotationMark
^'			RightSingleQuotationMark (curly apostrophe)
^.			Ellipsis ("..." character)
	NOTE: ^. is deprecated and should not be used.  Use "..." instead.

^e			LatinSmallLetterEWithAcute
^E			LatinSmallLetterEWithCircumflex

^d			GreekCapitalLetterDelta

^L			\
^V			|
^#			CommandKey
^`			`
^^			^

^0 - ^9		Replacement point for "replaceHat" function

{repl}		Replacement point for "replacePlaceholder" function

^U+nnnn		Unicode code point U+nnnn
\endcode
	
	\return (string) The localized display string with substitutions
--]]

function WFZString.fromZString( --[[string]] key, ... )

	check( 'string|table', '?string', '?string', '?string', '?string', '?string',
		   '?string', '?string', '?string', '?string' )

	local hatOffset = 0
	local keyIsTable = type( key ) == 'table'
	
	if not keyIsTable and string_find( key, '%^0' ) then
		-- This is a Lr-style ZSTR, which uses zero-based subsitution indices
		hatOffset = -1
	end

	makeTranslationMapOnce()

	local replacementMap
	if keyIsTable then
		replacementMap = key
		key = replacementMap[ 1 ]
	end
	
	local str = translateWithMap( key, translationMap, 3 )
		
	str = stringWithReplacedHatSequences( str )
	
	if replacementMap then
		for k, v in pairs( replacementMap ) do
			if k ~= 1 then
				str = replacePlaceholder( str, k, v )
			end
		end
	end
	
	local numReplacements = select( '#', ... )
	if numReplacements > 0 then
		for i = 1, numReplacements do
			str = replaceHat( str, i + hatOffset, select( i, ... ) )
		end
	end
	
	if WFZString_DEBUG_LOCALIZATION then
		str = str .. "_ZZZ"
	end
	
	return str
	
end

--------------------------------------------------------------------------------

function WFZString.fromNativeCodeZString( key )

	check( 'string' )

	makeTranslationMapOnce()
	
	local str = translateWithMap( key, translationMap, 3 )
	
	str = stringWithReplacedHatSequences( str )

	if WFZString_DEBUG_LOCALIZATION then
		str = str .. "_ZZZ"
	end
	
	return str
	
end

--------------------------------------------------------------------------------

return WFZString
