Version 0.9.0-rc1
- enable/disable from slash command - matching conditions (never, always, start, end, start or end) - support capturing groups - import examples - testing capabilities - compatibility with WoW Retail - adapted help texts - spelling errors
This commit is contained in:
@@ -6,9 +6,9 @@ local Grichelde = _G.Grichelde
|
||||
local L = LibStub('AceLocale-3.0'):NewLocale(AddonName, 'enUS', true)
|
||||
if not L then return end
|
||||
|
||||
local cYellow = Grichelde.functions.cYellow
|
||||
local cGray = Grichelde.functions.cGray
|
||||
local cPrefix = Grichelde.functions.cPrefix
|
||||
local cYellow = Grichelde.F.cYellow
|
||||
local cGray = Grichelde.F.cGray
|
||||
local cPrefix = Grichelde.F.cPrefix
|
||||
|
||||
-- system messages
|
||||
L.AddonName = "Grichelde"
|
||||
@@ -17,6 +17,8 @@ L.AddonLoaded = "%s happily assists you with your spelling disabilities now."
|
||||
L.AddonUnloaded = "%s patiently waits to support you again when needed."
|
||||
L.Upgrade_ToVersion = "Upgrade database to version %s."
|
||||
L.Upgrade_Successful = "Upgrade successful."
|
||||
L.Upgrade_Error = "Upgrade failed!"
|
||||
L.Downgrade_Detected = "Downgrade detected, %s might not work correctly!"
|
||||
|
||||
-- debug
|
||||
L.Debug_Options = "Options"
|
||||
@@ -41,6 +43,7 @@ L.Profiles_Copied = "Settings applied from profile %s."
|
||||
L.Profiles_Reset = "Profil %s reset."
|
||||
L.Profiles_Invalid = "Invalid profile %s!"
|
||||
L.Profiles_DeleteError = "The active profile cannot be deleted!"
|
||||
L.Profiles_AlreadyExistsError = "The profile %s already exists!"
|
||||
|
||||
-- minimap
|
||||
L.Minimap_Tooltip_Enabled = "%s"
|
||||
@@ -100,8 +103,19 @@ L.Options_Replacements_Header = "All matches on the lefthand side of the arrow \
|
||||
L.Options_Mapping_Group_Name = "%s => %s"
|
||||
L.Options_Mapping_Group_Desc = "This lookup will be replaced in activated channels."
|
||||
L.Options_Mapping_EmptyMapping = "(none)"
|
||||
L.Options_Mapping_Enabled_Name = "active"
|
||||
L.Options_Mapping_Enabled_Desc = "This replacement will be processed."
|
||||
L.Options_Mapping_MoveUp_Name = "^"
|
||||
L.Options_Mapping_MoveUp_Desc = "move up"
|
||||
L.Options_Mapping_MoveDown_Name = "v"
|
||||
L.Options_Mapping_MoveDown_Desc = "move down"
|
||||
L.Options_Mapping_MatchWhen_Name = "when:"
|
||||
L.Options_Mapping_MatchWhen_Desc = "Replacement is only done if the search text matches either |nanywhere (<always>), |nif the search text mantches <as a whole word>, |nolny at the <start of each word>, |nor at the <end of each word>, |nor <only at the start or end of each word> but not in between, |nor only in the middle of each word, but <never at start or end of any word>."
|
||||
L.Options_Mapping_MatchWhen_Select1 = "never (disabled)"
|
||||
L.Options_Mapping_MatchWhen_Select2 = "always"
|
||||
L.Options_Mapping_MatchWhen_Select3 = "as a whole word"
|
||||
L.Options_Mapping_MatchWhen_Select4 = "start of each word"
|
||||
L.Options_Mapping_MatchWhen_Select5 = "end of each word"
|
||||
L.Options_Mapping_MatchWhen_Select6 = "only at start and end of each word"
|
||||
L.Options_Mapping_MatchWhen_Select7 = "never at start or end of any word"
|
||||
L.Options_Mapping_SearchText_Name = "Search for:"
|
||||
L.Options_Mapping_SearchText_Desc = "This text is looked up in your chat input box."
|
||||
L.Options_Mapping_ReplaceText_Name = "Replacement:"
|
||||
@@ -112,10 +126,6 @@ L.Options_Mapping_Consolidate_Name = "consolidate consecutive matches"
|
||||
L.Options_Mapping_Consolidate_Desc = "If after the replacement a text sequence is repeated|ndirectly after another, treat them as one occurrence."
|
||||
L.Options_Mapping_StopOnMatch_Name = "stop on match"
|
||||
L.Options_Mapping_StopOnMatch_Desc = "Stops looking for any following replacements, when this one matched."
|
||||
L.Options_Mapping_MoveUp_Name = "^"
|
||||
L.Options_Mapping_MoveUp_Desc = "move up"
|
||||
L.Options_Mapping_MoveDown_Name = "v"
|
||||
L.Options_Mapping_MoveDown_Desc = "move down"
|
||||
L.Options_Mapping_Delete_Name = "Delete"
|
||||
L.Options_Mapping_Delete_Desc = "Deletes this replacement mapping."
|
||||
L.Options_Mapping_Delete_ConfirmText = "Delete this replacement mapping?"
|
||||
@@ -158,38 +168,440 @@ L.Options_Help_Expert = cYellow("shortening/lengthening replacements")
|
||||
.. "In the chatbox put " .. cPrefix("/gri") .. " or " .. cPrefix("/grichelde") .. " in front of your typed text, you can also include the target channel, "
|
||||
.. "i.e. " .. cPrefix("\"/gri /guild hello there\"") .. " and the active replacements are applied even if the guild channel or global switch was disabled."
|
||||
.. "|n|n" .. cYellow("Regular Expressions")
|
||||
.. "|nRegex are very powerful search and replacement patterns commonly used in programming. Technically all searches the addon performs on the input text are done with regular expression methods. "
|
||||
.. "Entering regex as search text however is an unofficial feature and has two major caveats: "
|
||||
.. "|n1. Unfornately Lua does not support full PCRE syntax and is very limited. Nethertheless some patterns can be used like start of line " .. cPrefix("\"^\"") .. " or end of line " .. cPrefix("\"$\"") .. ", "
|
||||
.. "character classes like numbers " .. cPrefix("\"%d\"") .. " or (inversed) sets " .. cPrefix("\"[^%p]\"") .. ". "
|
||||
.. "|n2. There is no support for capture groups in the replacement text, so matches get lost. Because of case sensivity and complexity there are no plans to support this."
|
||||
.. "|nAnyway, there are some mappings using RegEx in the Example secion."
|
||||
|
||||
.. "|nRegEx are very powerful search and replacement patterns commonly used in programming. Technically all searches the addon performs on the input are done with regular expression methods, "
|
||||
.. "however Lua unfortunately does not support full PCRE syntax and is very limited. Nevertheless some patterns can be used like the anchors start of line " .. cPrefix("\"^\"") .. " or end of line " .. cPrefix("\"$\"")
|
||||
.. ", capturing groups " .. cPrefix("\"(hello) (world)\"") .. "character classes like numbers " .. cPrefix("\"%d\"") .. " or (inversed) sets " .. cPrefix("\"[^%p]\"") .. ". "
|
||||
.. "Capture groups can be accessed in the replacement text with %<number> like in " .. cPrefix("\"%2 %1\"").. "."
|
||||
.. "|nAlso there are some mappings using RegEx in the Example section."
|
||||
|
||||
L.Options_Help_Examples_Note = cYellow("Note:") .. " This addon does not encourange or intend to hurt or to tease people with speaking disabilities or language disorders. The responsibility rest on the user completely. Please use the features with care and respect to other players."
|
||||
L.Options_Help_Examples0_Header = cYellow("Example")
|
||||
L.Options_Help_Examples0_Text = "Select an example from the dropdown above."
|
||||
L.Options_Help_Examples1_Select = "absent jaw"
|
||||
L.Options_Help_Examples1_Header = cYellow("S and P will be replaced by sibilant and clack sounds.")
|
||||
L.Options_Help_Examples1_Text = cPrefix("s => ch") .. "|n|n" .. cPrefix("t => ck")
|
||||
L.Options_Help_Examples2_Select = "trollifier"
|
||||
L.Options_Help_Examples2_Header = cYellow("Almost sound like a real Troll.")
|
||||
L.Options_Help_Examples2_Text = cPrefix("%.$ => , mon.") .. "|n|n" .. cPrefix("th => d") .. "|n|n" .. cPrefix("you => ya") .. "|n|n" .. cPrefix("ing => in'")
|
||||
L.Options_Help_Examples3_Select = "old-fashioned"
|
||||
L.Options_Help_Examples3_Header = cYellow("Use an outdate pronounciation.")
|
||||
L.Options_Help_Examples3_Text = cPrefix("oi => oy") .. "|n|n" .. cPrefix("do => doe") .. "|n|n" .. cPrefix("go => goe") .. "|n|n" .. cPrefix("you => thou") .. "|n|n" .. cPrefix("yours => thy") .. "|n|n" .. cPrefix("be => bee") .. "|n|n" .. cPrefix("we => wee")
|
||||
L.Options_Help_Examples4_Select = "abbreviations"
|
||||
L.Options_Help_Examples4_Header = cYellow("Say much, type less.")
|
||||
L.Options_Help_Examples4_Text = cPrefix("gz => Congratulations") .. "|n|n" .. cPrefix("gn8 => Good night") .. "|n|n" .. cPrefix("afk => I'm temporarikly not available (AFK)") .. "|n|n" .. cPrefix("MC => Molten Core")
|
||||
L.Options_Help_Examples5_Select = "Proper names"
|
||||
L.Options_Help_Examples5_Header = cYellow("Replace player names, NPCs or locations.")
|
||||
L.Options_Help_Examples5_Text = "exact case is recommended|n|n" .. cPrefix("Sylvanas => the revengeful banshee queen") .. "|n|n" .. cPrefix("Asmongold => Asmon") .. "|n|n" .. cPrefix("Crossroads => X-roads")
|
||||
L.Options_Help_Examples6_Select = "lisp"
|
||||
L.Options_Help_Examples6_Header = cYellow("S and Z will become a sibilant")
|
||||
L.Options_Help_Examples6_Text = cPrefix("s => th") .. "|n|n" .. cPrefix("ch => tsh") .. "|n|n" .. cPrefix("z => tsh") .. "|n|n" .. cPrefix("dg => ck")
|
||||
L.Options_Help_Examples7_Select = "stammer"
|
||||
L.Options_Help_Examples7_Header = cYellow("stammer")
|
||||
L.Options_Help_Examples7_Text = "p[% s]-$"
|
||||
L.Options_Help_Examples_Header = cYellow("Example")
|
||||
L.Options_Help_Examples_Text = "Select an example from the dropdown above."
|
||||
L.Options_Help_Examples_Import_Name = "Import"
|
||||
L.Options_Help_Examples_Import_Desc = "Imports the selected example into a new profile."
|
||||
L.Options_Help_Examples_Import_ConfirmText = "This will import the example %s into the nre profile %s."
|
||||
|
||||
L.Options_Help_Examples = {
|
||||
{
|
||||
name = "absent jaw",
|
||||
desc = cYellow("S and P will be replaced by sibilant and clack sounds."),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "s",
|
||||
replaceText = "ch",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "t",
|
||||
replaceText = "ck",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "p",
|
||||
replaceText = "b",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name = "trollifier",
|
||||
desc = cYellow("Almost sound like Vol'jin."),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "(%w)(%p?)$",
|
||||
replaceText = "%1, mon%2",
|
||||
exactCase = false,
|
||||
consolidate = false,
|
||||
matchWhen = 5,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "th",
|
||||
replaceText = "d",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "what are you",
|
||||
replaceText = "whatcha",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_13 = {
|
||||
order = 13,
|
||||
searchText = "your?s?",
|
||||
replaceText = "ya",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_14 = {
|
||||
order = 14,
|
||||
searchText = "going to",
|
||||
replaceText = "gonna",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_15 = {
|
||||
order = 15,
|
||||
searchText = "want to",
|
||||
replaceText = "wanna",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_16 = {
|
||||
order = 16,
|
||||
searchText = "ing",
|
||||
replaceText = "in'",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 5,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name = "Jar Jar Binks",
|
||||
desc = cYellow("Lets you sound like a clumsy Gungan"),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "me",
|
||||
replaceText = "mesa",
|
||||
exactCase = false,
|
||||
consolidate = false,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "I am",
|
||||
replaceText = "Mesa",
|
||||
exactCase = true,
|
||||
consolidate = false,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "I'm",
|
||||
replaceText = "Mesa",
|
||||
exactCase = true,
|
||||
consolidate = false,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_13 = {
|
||||
order = 13,
|
||||
searchText = "I",
|
||||
replaceText = "Me",
|
||||
exactCase = true,
|
||||
consolidate = false,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_14 = {
|
||||
order = 14,
|
||||
searchText = "you are",
|
||||
replaceText = "yousa",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_15 = {
|
||||
order = 15,
|
||||
searchText = "you're",
|
||||
replaceText = "yousa",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_16 = {
|
||||
order = 16,
|
||||
searchText = "your",
|
||||
replaceText = "yous",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_17 = {
|
||||
order = 17,
|
||||
searchText = "(s?)he is",
|
||||
replaceText = "%1hesa",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_18 = {
|
||||
order = 18,
|
||||
searchText = "(s?)he's",
|
||||
replaceText = "%1hesa",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_19 = {
|
||||
order = 19,
|
||||
searchText = "they",
|
||||
replaceText = "daysa",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_20 = {
|
||||
order = 20,
|
||||
searchText = "them",
|
||||
replaceText = "them-sa",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_21 = {
|
||||
order = 21,
|
||||
searchText = "ing",
|
||||
replaceText = "in'",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 5,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_22 = {
|
||||
order = 22,
|
||||
searchText = "the",
|
||||
replaceText = "da",
|
||||
exactCase = false,
|
||||
consolidate = false,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_23 = {
|
||||
order = 23,
|
||||
searchText = "th",
|
||||
replaceText = "d",
|
||||
exactCase = false,
|
||||
consolidate = false,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_24 = {
|
||||
order = 24,
|
||||
searchText = "yes",
|
||||
replaceText = "yesa",
|
||||
exactCase = false,
|
||||
consolidate = false,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_25 = {
|
||||
order = 25,
|
||||
searchText = "oka?y?",
|
||||
replaceText = "okeeday",
|
||||
exactCase = false,
|
||||
consolidate = false,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name = "old-fashioned",
|
||||
desc = cYellow("Use an outdated pronounciation."),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "oi",
|
||||
replaceText = "oy",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "([^aeiou]*)([aeiou])",
|
||||
replaceText = "%1%2e",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 5,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "yours",
|
||||
replaceText = "thy",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_13 = {
|
||||
order = 13,
|
||||
searchText = "youe",
|
||||
replaceText = "thou",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name = "abbreviations",
|
||||
desc = cYellow("Say much, type less."),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "gz",
|
||||
replaceText = "Congratulations",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "gn8",
|
||||
replaceText = "Good night",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "afk",
|
||||
replaceText = "I'm temporarily unavailable (AFK)",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_13 = {
|
||||
order = 13,
|
||||
searchText = "MC",
|
||||
replaceText = "Molten Core",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name = "Proper names",
|
||||
desc = cYellow("Replace player names, NPCs or locations."),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "Sylvanas",
|
||||
replaceText = "the revengeful banshee queen",
|
||||
exactCase = true,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "Asmon",
|
||||
replaceText = "Asmongold",
|
||||
exactCase = true,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "x%-?roads",
|
||||
replaceText = "Crossroads",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 3,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name = "Lisp",
|
||||
desc = cYellow("S and Z will become sibilant"),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "s",
|
||||
replaceText = "th",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_11 = {
|
||||
order = 11,
|
||||
searchText = "ch",
|
||||
replaceText = "tsh",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_12 = {
|
||||
order = 12,
|
||||
searchText = "z",
|
||||
replaceText = "tsh",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
replacement_13 = {
|
||||
order = 13,
|
||||
searchText = "dg",
|
||||
replaceText = "ck",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 2,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name = "stammer",
|
||||
desc = cYellow("Repeats vowels at the beginning of a sentence"),
|
||||
replacements = {
|
||||
replacement_10 = {
|
||||
order = 10,
|
||||
searchText = "^([^aeiouy]*)([aeiouy])",
|
||||
replaceText = "%1%2-%1%2-%1%2",
|
||||
exactCase = false,
|
||||
consolidate = true,
|
||||
matchWhen = 4,
|
||||
stopOnMatch = false,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
L.IgnorePattern_Star = "Star"
|
||||
L.IgnorePattern_Circle = "Circle"
|
||||
|
||||
Reference in New Issue
Block a user