InGameAvatarEditor/src/ServerScriptService/AvatarEditorInGameSetup/AvatarEditorInGame/Modules/Packages/Localization/LocalizationContext.lua
--[[
	Contains all of the loaded translations and provides methods to translate
	keys and parameters to strings.

	LocalizationContext doesn't handle loading of specific languages, but does
	recommend what languages should be loaded (if available).

	To create a new LocalizationContext:

		local currentLanguage = LocalizationService.RobloxLocaleId
		local languages = LocalizationContext.getRelevantLanguages(currentLanguage)

		local translations = {}

		-- Use the list of languages to load a set of translation tables here.
		-- A translation table is just a map from key to the translated string.
		-- `translations` is a map from language to translation tables.

		local context = LocalizationContext.new(translations)

		-- Get a string that doesn't require parameters
		context:getString(currentLanguage, "SOME_KEY")

		-- Passing parameters:
		context:getString(currentLanguage, "FANCY_KEY", {
			apples = 5,
		})

	Additional languages can be added after the LocalizationContext is created
	by calling `addTranslations`. Whenever the user's language changes, call
	`getRelevantLanguages` to get a new list of languages to load, load them,
	then call `addTranslations` to merge them in with the existing tables.
]]

--[[
	Finds the base language code for the given language, if there is one.

	We assume:
		* Language codes are of the form LANGUAGE or LANGUAGE_COUNTRY
		* LANGUAGE_COUNTRY is more specific than LANGUAGE
]]

local function getBaseLanguage(languageName)
	return languageName:match("^(%w+)[-_]")
end

local LocalizationContext = {}
LocalizationContext.__index = LocalizationContext

function LocalizationContext.new(translations)
	local self = {
		_translations = translations,
	}

	setmetatable(self, LocalizationContext)

	return self
end

--[[
	Add translations to an existing LocalizationContext, such as when a user
	switches languages while the app is running.
]]
function LocalizationContext:addTranslations(translations)
	self._translations = translations
end

--[[
	Yields a list of languages relevant to the current user.

	When the user's language changes, query this value, load those translations,
	and add them to the LocalizationContext using addTranslations.
]]
function LocalizationContext.getRelevantLanguages(primaryLanguage)
	local languages = {}

	-- Load the language itself if available.
	table.insert(languages, primaryLanguage)

	-- If there's a fallback for our current language, load that as well.
	local fallbackLanguage = getBaseLanguage(primaryLanguage)
	if fallbackLanguage then
		table.insert(languages, fallbackLanguage)
	end

	-- We should always load English, as it should contain every valid key.
	table.insert(languages, "en-us")
	return languages
end

function LocalizationContext:_getSourceString(language, key)
	local translationTable = self._translations[language]

	if not translationTable then
		return nil
	end

	return translationTable[key]
end

--[[
	Translate a key with a set of arguments into the given language.

	`language` must be explicitly provided
]]
function LocalizationContext:getString(language, key, parameters)
	local exactValue = self:_getSourceString(language, key)

	local baseLanguage = getBaseLanguage(language)

	local baseLanguageValue
	if baseLanguage then
		baseLanguageValue = self:_getSourceString(baseLanguage, key)
	end

	local englishValue = self:_getSourceString("en-us", key)

	-- We try to find source strings in descending priority here:
	local sourceString = exactValue or baseLanguageValue or englishValue

	-- Missing translations are considered a developer error, so we throw here.
	if not sourceString then
		local message = (
			"Couldn't find value for translation key %q!\n" ..
			"Tried these languages: %s, %s, %s"
		):format(
			key,
			language, baseLanguage, "en-us"
		)
		error(message, 2)
	end

	-- If we have parameters to insert into the string, put them in!
	-- We don't check for missing parameters, should we in the future?
	if parameters then
		return (sourceString:gsub("{(.-)}", parameters))
	else
		return sourceString
	end
end

return LocalizationContext