code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-05 16:12+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Chinese (http://www.transifex.com/gnu-social/gnu-social/language/zh/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:101
msgid "You will attend this event."
msgstr ""

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:105
msgid "You will not attend this event."
msgstr ""

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:109
msgid "You might attend this event."
msgstr ""

#. TRANS: Button text to cancel responding to an RSVP ("please respond") item.
#: forms/cancelrsvp.php:124
msgctxt "BUTTON"
msgid "Cancel"
msgstr ""

#. TRANS: Field label on form to RSVP ("please respond") for an event.
#: forms/rsvp.php:97
msgid "RSVP:"
msgstr ""

#. TRANS: Button text for RSVP ("please respond") reply to confirm attendence.
#: forms/rsvp.php:113
msgctxt "BUTTON"
msgid "Yes"
msgstr ""

#. TRANS: Button text for RSVP ("please respond") reply to deny attendence.
#: forms/rsvp.php:115
msgctxt "BUTTON"
msgid "No"
msgstr ""

#. TRANS: Button text for RSVP ("please respond") reply to indicate one might
#. attend.
#: forms/rsvp.php:117
msgctxt "BUTTON"
msgid "Maybe"
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:103
msgctxt "LABEL"
msgid "Title"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:106
msgid "Title of the event."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:118
msgctxt "LABEL"
msgid "Start date"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:121
msgid "Date the event starts."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:134
msgctxt "LABEL"
msgid "Start time"
msgstr ""

#. TRANS: Field title on event form. %s is the abbreviated timezone
#: forms/event.php:137
#, php-format
msgid "Time the event starts (%s)."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:151
msgctxt "LABEL"
msgid "End date"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:154
msgid "Date the event ends."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:163
msgctxt "LABEL"
msgid "End time"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:166
msgid "Time the event ends."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:175
msgctxt "LABEL"
msgid "Where?"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:178
msgid "Event location."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:185
msgctxt "LABEL"
msgid "URL"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:188
msgid "URL for more information."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:195
msgctxt "LABEL"
msgid "Description"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:198
msgid "Description of the event."
msgstr ""

#. TRANS: Button text to save an event..
#: forms/event.php:221
msgctxt "BUTTON"
msgid "Save"
msgstr ""

#. TRANS: Title for new event form.
#: actions/newevent.php:66
msgctxt "TITLE"
msgid "New event"
msgstr ""

#. TRANS: Client exception thrown when trying to post an event while not
#. logged in.
#: actions/newevent.php:84
msgid "Must be logged in to post a event."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a title.
#: actions/newevent.php:98
msgid "Title required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a start date.
#: actions/newevent.php:110
msgid "Start date required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing an end date.
#: actions/newevent.php:123
msgid "End date required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event with a date
#. that cannot be processed.
#. TRANS: %s is the data that could not be processed.
#: actions/newevent.php:141 actions/newevent.php:148
#, php-format
msgid "Could not parse date \"%s\"."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a title.
#: actions/newevent.php:196
msgid "Event must have a title."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a start time.
#: actions/newevent.php:201
msgid "Event must have a start time."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing an end time.
#: actions/newevent.php:206
msgid "Event must have an end time."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event with an invalid
#. URL.
#: actions/newevent.php:211
msgid "URL must be valid."
msgstr ""

#. TRANS: Page title after sending a notice.
#. TRANS: Page title after creating an event.
#: actions/newevent.php:250 actions/cancelrsvp.php:156 actions/newrsvp.php:161
msgid "Event saved"
msgstr ""

#. TRANS: Title for RSVP ("please respond") action.
#: actions/cancelrsvp.php:61
msgctxt "TITLE"
msgid "Cancel RSVP"
msgstr ""

#. TRANS: Client exception thrown when referring to a non-existing RSVP
#. ("please respond") item.
#. TRANS: Client exception thrown when referring to a non-existing RSVP.
#. TRANS: RSVP stands for "Please reply".
#: actions/cancelrsvp.php:82 actions/cancelrsvp.php:89 actions/showrsvp.php:61
#: actions/showrsvp.php:77
msgid "No such RSVP."
msgstr ""

#. TRANS: Client exception thrown when referring to a non-existing event.
#: actions/cancelrsvp.php:96 actions/showevent.php:60 actions/showevent.php:68
#: actions/newrsvp.php:82 actions/newrsvp.php:89 actions/showrsvp.php:68
msgid "No such event."
msgstr ""

#. TRANS: Client exception thrown when trying tp RSVP ("please respond") while
#. not logged in.
#. TRANS: Client exception thrown when trying to RSVP ("please respond") while
#. not logged in.
#: actions/cancelrsvp.php:103 actions/newrsvp.php:96
msgid "You must be logged in to RSVP for an event."
msgstr ""

#. TRANS: Title for RSVP ("please respond") action.
#: actions/newrsvp.php:61
msgctxt "TITLE"
msgid "New RSVP"
msgstr ""

#. TRANS: Client exception thrown when using an invalid value for RSVP
#. ("please respond").
#: actions/newrsvp.php:113
msgid "Unknown submit value."
msgstr ""

#. TRANS: Title for event.
#. TRANS: %1$s is a user nickname, %2$s is an event title.
#: actions/showrsvp.php:94
#, php-format
msgid "%1$s's RSVP for \"%2$s\""
msgstr ""

#. TRANS: Error message displayed when trying to perform an action that
#. requires a logged in user.
#: actions/timelist.php:65
msgid "Not logged in."
msgstr ""

#. TRANS: Client error when submitting a form with unexpected information.
#: actions/timelist.php:72
msgid "Unexpected form submission."
msgstr ""

#. TRANS: Client error displayed when using an action in a non-AJAX way.
#: actions/timelist.php:80
msgid "This action is AJAX only."
msgstr ""

#. TRANS: Client exception thrown when trying to create an event that already
#. exists.
#: classes/Happening.php:111
msgid "Event already exists."
msgstr ""

#. TRANS: Event description. %1$s is a title, %2$s is start time, %3$s is end
#. time,
#. TRANS: %4$s is location, %5$s is a description.
#: classes/Happening.php:145
#, php-format
msgid "\"%1$s\" %2$s - %3$s (%4$s): %5$s"
msgstr ""

#. TRANS: Rendered microformats2 tagged event description.
#. TRANS: %1$s is a title, %2$s is start time, %3$s is start time,
#. TRANS: %4$s is end time, %5$s is end time, %6$s is location, %7$s is
#. description.
#. TRANS: Class names should not be translated.
#: classes/Happening.php:156
#, php-format
msgid ""
"

%1$s
 <time class"
"=\"dt-start\" datetime=\"%2$s\">%3$s</time> - <time class=\"dt-end\" "
"datetime=\"%4$s\">%5$s</time> (%6$s):
%7$s

"
msgstr ""

#. TRANS: Client exception thrown when trying to save an already existing RSVP
#. ("please respond").
#: classes/RSVP.php:116 classes/RSVP.php:125
msgid "RSVP already exists."
msgstr ""

#. TRANS: Exception thrown when requesting an undefined verb for RSVP.
#: classes/RSVP.php:194
#, php-format
msgid "Unknown verb \"%s\"."
msgstr ""

#. TRANS: Exception thrown when requesting an undefined code for RSVP.
#: classes/RSVP.php:212
#, php-format
msgid "Unknown code \"%s\"."
msgstr ""

#. TRANS: Server exception thrown when requesting a non-exsting notice for an
#. RSVP ("please respond").
#. TRANS: %s is the RSVP with the missing notice.
#: classes/RSVP.php:222
#, php-format
msgid "RSVP %s does not correspond to a notice in the database."
msgstr ""

#. TRANS: Exception thrown when requesting a non-existing profile.
#. TRANS: %s is the ID of the non-existing profile.
#: classes/RSVP.php:284
#, php-format
msgid "No profile with ID %s."
msgstr ""

#. TRANS: Exception thrown when requesting a non-existing event.
#. TRANS: %s is the ID of the non-existing event.
#: classes/RSVP.php:295
#, php-format
msgid "No event with ID %s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:327
#, php-format
msgid ""
"%2$s is attending %4$s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:333
#, php-format
msgid ""
"%2$s is not attending "
"%4$s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:339
#, php-format
msgid ""
"%2$s might attend %4$s."
msgstr ""

#. TRANS: Exception thrown when requesting a user's RSVP status for a non-
#. existing response code.
#. TRANS: %s is the non-existing response code.
#: classes/RSVP.php:344 classes/RSVP.php:388
#, php-format
msgid "Unknown response code %s."
msgstr ""

#. TRANS: Used as event title when not event title is available.
#. TRANS: Used as: Username [is [not ] attending|might attend] an unknown
#. event.
#: classes/RSVP.php:351 classes/RSVP.php:395
msgid "an unknown event"
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:373
#, php-format
msgid "%1$s is attending %2$s."
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:378
#, php-format
msgid "%1$s is not attending %2$s."
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:383
#, php-format
msgid "%1$s might attend %2$s."
msgstr ""

#. TRANS: Plugin description.
#: EventPlugin.php:101
msgid "Event invitations and RSVPs."
msgstr ""

#. TRANS: Title for event application.
#: EventPlugin.php:107
msgctxt "TITLE"
msgid "Event"
msgstr ""

#. TRANS: Exception thrown when there are too many activity objects.
#: EventPlugin.php:135
msgid "Too many activity objects."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a non-event type
#. object.
#: EventPlugin.php:142
msgid "Wrong type for object."
msgstr ""

#. TRANS: Exception thrown when trying to RSVP for an unknown event.
#: EventPlugin.php:166
msgid "RSVP for unknown event."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a undefined verb.
#: EventPlugin.php:172
msgid "Unknown verb for events."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a unknown object
#. type.
#: EventPlugin.php:203
msgid "Unknown object type."
msgstr ""

#. TRANS: Exception thrown when referring to a notice that is not an event an
#. in event context.
#: EventPlugin.php:210
msgid "Unknown event notice."
msgstr ""

#. TRANS: Content for a deleted RSVP list item (RSVP stands for "please
#. respond").
#: EventPlugin.php:342 EventPlugin.php:474
msgid "Deleted."
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:389
msgid "Time:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:406
msgid "Location:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:414
msgid "Description:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:424
msgid "Attending:"
msgstr ""

#. TRANS: 0 minutes abbreviated. Used in a list.
#: lib/eventtimelist.php:85
msgid "(0 min)"
msgstr ""

#. TRANS: 30 minutes abbreviated. Used in a list.
#: lib/eventtimelist.php:89
msgid "(30 min)"
msgstr ""

#. TRANS: 1 hour. Used in a list.
#: lib/eventtimelist.php:93
msgid "(1 hour)"
msgstr ""

#: lib/eventtimelist.php:98
#, php-format
msgid "(%.1f hours)"
msgstr ""

#: lib/eventtimelist.php:99
#, php-format
msgid "(%d hours)"
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-05 16:12+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Chinese (China) (http://www.transifex.com/gnu-social/gnu-social/language/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_CN\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:101
msgid "You will attend this event."
msgstr ""

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:105
msgid "You will not attend this event."
msgstr ""

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:109
msgid "You might attend this event."
msgstr ""

#. TRANS: Button text to cancel responding to an RSVP ("please respond") item.
#: forms/cancelrsvp.php:124
msgctxt "BUTTON"
msgid "Cancel"
msgstr "取消"

#. TRANS: Field label on form to RSVP ("please respond") for an event.
#: forms/rsvp.php:97
msgid "RSVP:"
msgstr ""

#. TRANS: Button text for RSVP ("please respond") reply to confirm attendence.
#: forms/rsvp.php:113
msgctxt "BUTTON"
msgid "Yes"
msgstr "是"

#. TRANS: Button text for RSVP ("please respond") reply to deny attendence.
#: forms/rsvp.php:115
msgctxt "BUTTON"
msgid "No"
msgstr "否"

#. TRANS: Button text for RSVP ("please respond") reply to indicate one might
#. attend.
#: forms/rsvp.php:117
msgctxt "BUTTON"
msgid "Maybe"
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:103
msgctxt "LABEL"
msgid "Title"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:106
msgid "Title of the event."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:118
msgctxt "LABEL"
msgid "Start date"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:121
msgid "Date the event starts."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:134
msgctxt "LABEL"
msgid "Start time"
msgstr ""

#. TRANS: Field title on event form. %s is the abbreviated timezone
#: forms/event.php:137
#, php-format
msgid "Time the event starts (%s)."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:151
msgctxt "LABEL"
msgid "End date"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:154
msgid "Date the event ends."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:163
msgctxt "LABEL"
msgid "End time"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:166
msgid "Time the event ends."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:175
msgctxt "LABEL"
msgid "Where?"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:178
msgid "Event location."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:185
msgctxt "LABEL"
msgid "URL"
msgstr "URL"

#. TRANS: Field title on event form.
#: forms/event.php:188
msgid "URL for more information."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:195
msgctxt "LABEL"
msgid "Description"
msgstr "描述"

#. TRANS: Field title on event form.
#: forms/event.php:198
msgid "Description of the event."
msgstr ""

#. TRANS: Button text to save an event..
#: forms/event.php:221
msgctxt "BUTTON"
msgid "Save"
msgstr "保存"

#. TRANS: Title for new event form.
#: actions/newevent.php:66
msgctxt "TITLE"
msgid "New event"
msgstr ""

#. TRANS: Client exception thrown when trying to post an event while not
#. logged in.
#: actions/newevent.php:84
msgid "Must be logged in to post a event."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a title.
#: actions/newevent.php:98
msgid "Title required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a start date.
#: actions/newevent.php:110
msgid "Start date required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing an end date.
#: actions/newevent.php:123
msgid "End date required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event with a date
#. that cannot be processed.
#. TRANS: %s is the data that could not be processed.
#: actions/newevent.php:141 actions/newevent.php:148
#, php-format
msgid "Could not parse date \"%s\"."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a title.
#: actions/newevent.php:196
msgid "Event must have a title."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a start time.
#: actions/newevent.php:201
msgid "Event must have a start time."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing an end time.
#: actions/newevent.php:206
msgid "Event must have an end time."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event with an invalid
#. URL.
#: actions/newevent.php:211
msgid "URL must be valid."
msgstr ""

#. TRANS: Page title after sending a notice.
#. TRANS: Page title after creating an event.
#: actions/newevent.php:250 actions/cancelrsvp.php:156 actions/newrsvp.php:161
msgid "Event saved"
msgstr ""

#. TRANS: Title for RSVP ("please respond") action.
#: actions/cancelrsvp.php:61
msgctxt "TITLE"
msgid "Cancel RSVP"
msgstr ""

#. TRANS: Client exception thrown when referring to a non-existing RSVP
#. ("please respond") item.
#. TRANS: Client exception thrown when referring to a non-existing RSVP.
#. TRANS: RSVP stands for "Please reply".
#: actions/cancelrsvp.php:82 actions/cancelrsvp.php:89 actions/showrsvp.php:61
#: actions/showrsvp.php:77
msgid "No such RSVP."
msgstr ""

#. TRANS: Client exception thrown when referring to a non-existing event.
#: actions/cancelrsvp.php:96 actions/showevent.php:60 actions/showevent.php:68
#: actions/newrsvp.php:82 actions/newrsvp.php:89 actions/showrsvp.php:68
msgid "No such event."
msgstr ""

#. TRANS: Client exception thrown when trying tp RSVP ("please respond") while
#. not logged in.
#. TRANS: Client exception thrown when trying to RSVP ("please respond") while
#. not logged in.
#: actions/cancelrsvp.php:103 actions/newrsvp.php:96
msgid "You must be logged in to RSVP for an event."
msgstr ""

#. TRANS: Title for RSVP ("please respond") action.
#: actions/newrsvp.php:61
msgctxt "TITLE"
msgid "New RSVP"
msgstr ""

#. TRANS: Client exception thrown when using an invalid value for RSVP
#. ("please respond").
#: actions/newrsvp.php:113
msgid "Unknown submit value."
msgstr ""

#. TRANS: Title for event.
#. TRANS: %1$s is a user nickname, %2$s is an event title.
#: actions/showrsvp.php:94
#, php-format
msgid "%1$s's RSVP for \"%2$s\""
msgstr ""

#. TRANS: Error message displayed when trying to perform an action that
#. requires a logged in user.
#: actions/timelist.php:65
msgid "Not logged in."
msgstr "未登录。"

#. TRANS: Client error when submitting a form with unexpected information.
#: actions/timelist.php:72
msgid "Unexpected form submission."
msgstr "未预料的表单提交。"

#. TRANS: Client error displayed when using an action in a non-AJAX way.
#: actions/timelist.php:80
msgid "This action is AJAX only."
msgstr ""

#. TRANS: Client exception thrown when trying to create an event that already
#. exists.
#: classes/Happening.php:111
msgid "Event already exists."
msgstr ""

#. TRANS: Event description. %1$s is a title, %2$s is start time, %3$s is end
#. time,
#. TRANS: %4$s is location, %5$s is a description.
#: classes/Happening.php:145
#, php-format
msgid "\"%1$s\" %2$s - %3$s (%4$s): %5$s"
msgstr ""

#. TRANS: Rendered microformats2 tagged event description.
#. TRANS: %1$s is a title, %2$s is start time, %3$s is start time,
#. TRANS: %4$s is end time, %5$s is end time, %6$s is location, %7$s is
#. description.
#. TRANS: Class names should not be translated.
#: classes/Happening.php:156
#, php-format
msgid ""
"

%1$s
 <time class"
"=\"dt-start\" datetime=\"%2$s\">%3$s</time> - <time class=\"dt-end\" "
"datetime=\"%4$s\">%5$s</time> (%6$s):
%7$s

"
msgstr ""

#. TRANS: Client exception thrown when trying to save an already existing RSVP
#. ("please respond").
#: classes/RSVP.php:116 classes/RSVP.php:125
msgid "RSVP already exists."
msgstr ""

#. TRANS: Exception thrown when requesting an undefined verb for RSVP.
#: classes/RSVP.php:194
#, php-format
msgid "Unknown verb \"%s\"."
msgstr ""

#. TRANS: Exception thrown when requesting an undefined code for RSVP.
#: classes/RSVP.php:212
#, php-format
msgid "Unknown code \"%s\"."
msgstr ""

#. TRANS: Server exception thrown when requesting a non-exsting notice for an
#. RSVP ("please respond").
#. TRANS: %s is the RSVP with the missing notice.
#: classes/RSVP.php:222
#, php-format
msgid "RSVP %s does not correspond to a notice in the database."
msgstr ""

#. TRANS: Exception thrown when requesting a non-existing profile.
#. TRANS: %s is the ID of the non-existing profile.
#: classes/RSVP.php:284
#, php-format
msgid "No profile with ID %s."
msgstr ""

#. TRANS: Exception thrown when requesting a non-existing event.
#. TRANS: %s is the ID of the non-existing event.
#: classes/RSVP.php:295
#, php-format
msgid "No event with ID %s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:327
#, php-format
msgid ""
"%2$s is attending %4$s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:333
#, php-format
msgid ""
"%2$s is not attending "
"%4$s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:339
#, php-format
msgid ""
"%2$s might attend %4$s."
msgstr ""

#. TRANS: Exception thrown when requesting a user's RSVP status for a non-
#. existing response code.
#. TRANS: %s is the non-existing response code.
#: classes/RSVP.php:344 classes/RSVP.php:388
#, php-format
msgid "Unknown response code %s."
msgstr ""

#. TRANS: Used as event title when not event title is available.
#. TRANS: Used as: Username [is [not ] attending|might attend] an unknown
#. event.
#: classes/RSVP.php:351 classes/RSVP.php:395
msgid "an unknown event"
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:373
#, php-format
msgid "%1$s is attending %2$s."
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:378
#, php-format
msgid "%1$s is not attending %2$s."
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:383
#, php-format
msgid "%1$s might attend %2$s."
msgstr ""

#. TRANS: Plugin description.
#: EventPlugin.php:101
msgid "Event invitations and RSVPs."
msgstr ""

#. TRANS: Title for event application.
#: EventPlugin.php:107
msgctxt "TITLE"
msgid "Event"
msgstr ""

#. TRANS: Exception thrown when there are too many activity objects.
#: EventPlugin.php:135
msgid "Too many activity objects."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a non-event type
#. object.
#: EventPlugin.php:142
msgid "Wrong type for object."
msgstr ""

#. TRANS: Exception thrown when trying to RSVP for an unknown event.
#: EventPlugin.php:166
msgid "RSVP for unknown event."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a undefined verb.
#: EventPlugin.php:172
msgid "Unknown verb for events."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a unknown object
#. type.
#: EventPlugin.php:203
msgid "Unknown object type."
msgstr ""

#. TRANS: Exception thrown when referring to a notice that is not an event an
#. in event context.
#: EventPlugin.php:210
msgid "Unknown event notice."
msgstr ""

#. TRANS: Content for a deleted RSVP list item (RSVP stands for "please
#. respond").
#: EventPlugin.php:342 EventPlugin.php:474
msgid "Deleted."
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:389
msgid "Time:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:406
msgid "Location:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:414
msgid "Description:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:424
msgid "Attending:"
msgstr ""

#. TRANS: 0 minutes abbreviated. Used in a list.
#: lib/eventtimelist.php:85
msgid "(0 min)"
msgstr ""

#. TRANS: 30 minutes abbreviated. Used in a list.
#: lib/eventtimelist.php:89
msgid "(30 min)"
msgstr ""

#. TRANS: 1 hour. Used in a list.
#: lib/eventtimelist.php:93
msgid "(1 hour)"
msgstr ""

#: lib/eventtimelist.php:98
#, php-format
msgid "(%.1f hours)"
msgstr ""

#: lib/eventtimelist.php:99
#, php-format
msgid "(%d hours)"
msgstr ""

repo_name: spookie/gnu

license: mit


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-02-02 17:47+0100\n"
"PO-Revision-Date: 2015-02-05 16:12+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Chinese (Taiwan) (http://www.transifex.com/gnu-social/gnu-social/language/zh_TW/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh_TW\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:101
msgid "You will attend this event."
msgstr ""

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:105
msgid "You will not attend this event."
msgstr ""

#. TRANS: Possible status for RSVP ("please respond") item.
#: forms/cancelrsvp.php:109
msgid "You might attend this event."
msgstr ""

#. TRANS: Button text to cancel responding to an RSVP ("please respond") item.
#: forms/cancelrsvp.php:124
msgctxt "BUTTON"
msgid "Cancel"
msgstr ""

#. TRANS: Field label on form to RSVP ("please respond") for an event.
#: forms/rsvp.php:97
msgid "RSVP:"
msgstr ""

#. TRANS: Button text for RSVP ("please respond") reply to confirm attendence.
#: forms/rsvp.php:113
msgctxt "BUTTON"
msgid "Yes"
msgstr ""

#. TRANS: Button text for RSVP ("please respond") reply to deny attendence.
#: forms/rsvp.php:115
msgctxt "BUTTON"
msgid "No"
msgstr ""

#. TRANS: Button text for RSVP ("please respond") reply to indicate one might
#. attend.
#: forms/rsvp.php:117
msgctxt "BUTTON"
msgid "Maybe"
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:103
msgctxt "LABEL"
msgid "Title"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:106
msgid "Title of the event."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:118
msgctxt "LABEL"
msgid "Start date"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:121
msgid "Date the event starts."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:134
msgctxt "LABEL"
msgid "Start time"
msgstr ""

#. TRANS: Field title on event form. %s is the abbreviated timezone
#: forms/event.php:137
#, php-format
msgid "Time the event starts (%s)."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:151
msgctxt "LABEL"
msgid "End date"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:154
msgid "Date the event ends."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:163
msgctxt "LABEL"
msgid "End time"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:166
msgid "Time the event ends."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:175
msgctxt "LABEL"
msgid "Where?"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:178
msgid "Event location."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:185
msgctxt "LABEL"
msgid "URL"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:188
msgid "URL for more information."
msgstr ""

#. TRANS: Field label on event form.
#: forms/event.php:195
msgctxt "LABEL"
msgid "Description"
msgstr ""

#. TRANS: Field title on event form.
#: forms/event.php:198
msgid "Description of the event."
msgstr ""

#. TRANS: Button text to save an event..
#: forms/event.php:221
msgctxt "BUTTON"
msgid "Save"
msgstr ""

#. TRANS: Title for new event form.
#: actions/newevent.php:66
msgctxt "TITLE"
msgid "New event"
msgstr ""

#. TRANS: Client exception thrown when trying to post an event while not
#. logged in.
#: actions/newevent.php:84
msgid "Must be logged in to post a event."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a title.
#: actions/newevent.php:98
msgid "Title required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a start date.
#: actions/newevent.php:110
msgid "Start date required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing an end date.
#: actions/newevent.php:123
msgid "End date required."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event with a date
#. that cannot be processed.
#. TRANS: %s is the data that could not be processed.
#: actions/newevent.php:141 actions/newevent.php:148
#, php-format
msgid "Could not parse date \"%s\"."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a title.
#: actions/newevent.php:196
msgid "Event must have a title."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing a start time.
#: actions/newevent.php:201
msgid "Event must have a start time."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event without
#. providing an end time.
#: actions/newevent.php:206
msgid "Event must have an end time."
msgstr ""

#. TRANS: Client exception thrown when trying to post an event with an invalid
#. URL.
#: actions/newevent.php:211
msgid "URL must be valid."
msgstr ""

#. TRANS: Page title after sending a notice.
#. TRANS: Page title after creating an event.
#: actions/newevent.php:250 actions/cancelrsvp.php:156 actions/newrsvp.php:161
msgid "Event saved"
msgstr ""

#. TRANS: Title for RSVP ("please respond") action.
#: actions/cancelrsvp.php:61
msgctxt "TITLE"
msgid "Cancel RSVP"
msgstr ""

#. TRANS: Client exception thrown when referring to a non-existing RSVP
#. ("please respond") item.
#. TRANS: Client exception thrown when referring to a non-existing RSVP.
#. TRANS: RSVP stands for "Please reply".
#: actions/cancelrsvp.php:82 actions/cancelrsvp.php:89 actions/showrsvp.php:61
#: actions/showrsvp.php:77
msgid "No such RSVP."
msgstr ""

#. TRANS: Client exception thrown when referring to a non-existing event.
#: actions/cancelrsvp.php:96 actions/showevent.php:60 actions/showevent.php:68
#: actions/newrsvp.php:82 actions/newrsvp.php:89 actions/showrsvp.php:68
msgid "No such event."
msgstr ""

#. TRANS: Client exception thrown when trying tp RSVP ("please respond") while
#. not logged in.
#. TRANS: Client exception thrown when trying to RSVP ("please respond") while
#. not logged in.
#: actions/cancelrsvp.php:103 actions/newrsvp.php:96
msgid "You must be logged in to RSVP for an event."
msgstr ""

#. TRANS: Title for RSVP ("please respond") action.
#: actions/newrsvp.php:61
msgctxt "TITLE"
msgid "New RSVP"
msgstr ""

#. TRANS: Client exception thrown when using an invalid value for RSVP
#. ("please respond").
#: actions/newrsvp.php:113
msgid "Unknown submit value."
msgstr ""

#. TRANS: Title for event.
#. TRANS: %1$s is a user nickname, %2$s is an event title.
#: actions/showrsvp.php:94
#, php-format
msgid "%1$s's RSVP for \"%2$s\""
msgstr ""

#. TRANS: Error message displayed when trying to perform an action that
#. requires a logged in user.
#: actions/timelist.php:65
msgid "Not logged in."
msgstr "未登錄。"

#. TRANS: Client error when submitting a form with unexpected information.
#: actions/timelist.php:72
msgid "Unexpected form submission."
msgstr ""

#. TRANS: Client error displayed when using an action in a non-AJAX way.
#: actions/timelist.php:80
msgid "This action is AJAX only."
msgstr ""

#. TRANS: Client exception thrown when trying to create an event that already
#. exists.
#: classes/Happening.php:111
msgid "Event already exists."
msgstr ""

#. TRANS: Event description. %1$s is a title, %2$s is start time, %3$s is end
#. time,
#. TRANS: %4$s is location, %5$s is a description.
#: classes/Happening.php:145
#, php-format
msgid "\"%1$s\" %2$s - %3$s (%4$s): %5$s"
msgstr ""

#. TRANS: Rendered microformats2 tagged event description.
#. TRANS: %1$s is a title, %2$s is start time, %3$s is start time,
#. TRANS: %4$s is end time, %5$s is end time, %6$s is location, %7$s is
#. description.
#. TRANS: Class names should not be translated.
#: classes/Happening.php:156
#, php-format
msgid ""
"

%1$s
 <time class"
"=\"dt-start\" datetime=\"%2$s\">%3$s</time> - <time class=\"dt-end\" "
"datetime=\"%4$s\">%5$s</time> (%6$s):
%7$s

"
msgstr ""

#. TRANS: Client exception thrown when trying to save an already existing RSVP
#. ("please respond").
#: classes/RSVP.php:116 classes/RSVP.php:125
msgid "RSVP already exists."
msgstr ""

#. TRANS: Exception thrown when requesting an undefined verb for RSVP.
#: classes/RSVP.php:194
#, php-format
msgid "Unknown verb \"%s\"."
msgstr ""

#. TRANS: Exception thrown when requesting an undefined code for RSVP.
#: classes/RSVP.php:212
#, php-format
msgid "Unknown code \"%s\"."
msgstr ""

#. TRANS: Server exception thrown when requesting a non-exsting notice for an
#. RSVP ("please respond").
#. TRANS: %s is the RSVP with the missing notice.
#: classes/RSVP.php:222
#, php-format
msgid "RSVP %s does not correspond to a notice in the database."
msgstr ""

#. TRANS: Exception thrown when requesting a non-existing profile.
#. TRANS: %s is the ID of the non-existing profile.
#: classes/RSVP.php:284
#, php-format
msgid "No profile with ID %s."
msgstr ""

#. TRANS: Exception thrown when requesting a non-existing event.
#. TRANS: %s is the ID of the non-existing event.
#: classes/RSVP.php:295
#, php-format
msgid "No event with ID %s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:327
#, php-format
msgid ""
"%2$s is attending %4$s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:333
#, php-format
msgid ""
"%2$s is not attending "
"%4$s."
msgstr ""

#. TRANS: HTML version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile URL, %2$s a profile name,
#. TRANS: %3$s is an event URL, %4$s an event title.
#: classes/RSVP.php:339
#, php-format
msgid ""
"%2$s might attend %4$s."
msgstr ""

#. TRANS: Exception thrown when requesting a user's RSVP status for a non-
#. existing response code.
#. TRANS: %s is the non-existing response code.
#: classes/RSVP.php:344 classes/RSVP.php:388
#, php-format
msgid "Unknown response code %s."
msgstr ""

#. TRANS: Used as event title when not event title is available.
#. TRANS: Used as: Username [is [not ] attending|might attend] an unknown
#. event.
#: classes/RSVP.php:351 classes/RSVP.php:395
msgid "an unknown event"
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:373
#, php-format
msgid "%1$s is attending %2$s."
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:378
#, php-format
msgid "%1$s is not attending %2$s."
msgstr ""

#. TRANS: Plain text version of an RSVP ("please respond") status for a user.
#. TRANS: %1$s is a profile name, %2$s is an event title.
#: classes/RSVP.php:383
#, php-format
msgid "%1$s might attend %2$s."
msgstr ""

#. TRANS: Plugin description.
#: EventPlugin.php:101
msgid "Event invitations and RSVPs."
msgstr ""

#. TRANS: Title for event application.
#: EventPlugin.php:107
msgctxt "TITLE"
msgid "Event"
msgstr ""

#. TRANS: Exception thrown when there are too many activity objects.
#: EventPlugin.php:135
msgid "Too many activity objects."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a non-event type
#. object.
#: EventPlugin.php:142
msgid "Wrong type for object."
msgstr ""

#. TRANS: Exception thrown when trying to RSVP for an unknown event.
#: EventPlugin.php:166
msgid "RSVP for unknown event."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a undefined verb.
#: EventPlugin.php:172
msgid "Unknown verb for events."
msgstr ""

#. TRANS: Exception thrown when event plugin comes across a unknown object
#. type.
#: EventPlugin.php:203
msgid "Unknown object type."
msgstr ""

#. TRANS: Exception thrown when referring to a notice that is not an event an
#. in event context.
#: EventPlugin.php:210
msgid "Unknown event notice."
msgstr ""

#. TRANS: Content for a deleted RSVP list item (RSVP stands for "please
#. respond").
#: EventPlugin.php:342 EventPlugin.php:474
msgid "Deleted."
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:389
msgid "Time:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:406
msgid "Location:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:414
msgid "Description:"
msgstr ""

#. TRANS: Field label for event description.
#: EventPlugin.php:424
msgid "Attending:"
msgstr ""

#. TRANS: 0 minutes abbreviated. Used in a list.
#: lib/eventtimelist.php:85
msgid "(0 min)"
msgstr ""

#. TRANS: 30 minutes abbreviated. Used in a list.
#: lib/eventtimelist.php:89
msgid "(30 min)"
msgstr ""

#. TRANS: 1 hour. Used in a list.
#: lib/eventtimelist.php:93
msgid "(1 hour)"
msgstr ""

#: lib/eventtimelist.php:98
#, php-format
msgid "(%.1f hours)"
msgstr ""

#: lib/eventtimelist.php:99
#, php-format
msgid "(%d hours)"
msgstr ""


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social 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.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Extra profile bio-like fields and allows administrators to define
 * additional profile fields for the users of a GNU social installation.
 *
 * @category  Widget
 * @package   GNU social
 * @author    Brion Vibber <brion@status.net>
 * @author    Max Shinn <trombonechamp@gmail.com>
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2011-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

include_once __DIR__ . '/lib/profiletools.php';

class ExtendedProfilePlugin extends Plugin
{
    const PLUGIN_VERSION = '3.0.2';

    public function onPluginVersion(array &$versions): bool
    {
        $versions[] = [
            'name' => 'ExtendedProfile',
            'version' => self::PLUGIN_VERSION,
            'author' => 'Brion Vibber, Samantha Doherty, Zach Copley, Max Shinn, Diogo Cordeiro',
            'homepage' => GNUSOCIAL_ENGINE_REPO_URL . 'tree/master/plugins/ExtendedProfile',
            // TRANS: Module description.
            'rawdescription' => _m('UI extensions for additional profile fields.')
        ];

        return true;
    }

    /**
     * Add paths to the router table
     *
     * Hook for RouterInitialized event.
     *
     * @param URLMapper $m URL mapper
     *
     * @return bool hook return
     * @throws Exception
     */
    public function onStartInitializeRouter(URLMapper $m)
    {
        $m->connect(
            ':nickname/detail',
            ['action' => 'profiledetail'],
            ['nickname' => Nickname::DISPLAY_FMT]
        );
        $m->connect(
            '/settings/profile/finduser',
            ['action' => 'Userautocomplete']
        );
        $m->connect(
            'settings/profile/detail',
            ['action' => 'profiledetailsettings']
        );
        $m->connect(
            'panel/profilefields',
            ['action' => 'profilefieldsAdminPanel']
        );

        return true;
    }

    public function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('profile_detail', Profile_detail::schemaDef());
        $schema->ensureTable('gnusocialprofileextensionfield', GNUsocialProfileExtensionField::schemaDef());
        $schema->ensureTable('gnusocialprofileextensionresponse', GNUsocialProfileExtensionResponse::schemaDef());
        return true;
    }

    public function onEndShowAccountProfileBlock(HTMLOutputter $out, Profile $profile)
    {
        $user = User::getKV('id', $profile->id);
        if ($user) {
            $url = common_local_url('profiledetail', ['nickname' => $user->nickname]);
            // TRANS: Link text on user profile page leading to extended profile page.
            $out->element('a', ['href' => $url, 'class' => 'profiledetail'], _m('More details...'));
        }
    }

    /**
     * Menu item for personal subscriptions/groups area
     *
     * @param Action $action action being executed
     *
     * @return bool hook return
     * @throws Exception
     */
    public function onEndAccountSettingsNav(Action $action)
    {
        $action_name = $action->trimmed('action');

        $action->menuItem(
            common_local_url('profiledetailsettings'),
            // TRANS: Extended profile plugin menu item on user settings page.
            _m('MENU', 'Full Profile'),
            // TRANS: Extended profile plugin tooltip for user settings menu item.
            _m('Change your extended profile settings'),
            $action_name === 'profiledetailsettings'
        );

        return true;
    }

    /*public function onEndProfileFormData(Action $action): bool
    {
        $fields = GNUsocialProfileExtensionField::allFields();
        $user = common_current_user();
        $profile = $user->getProfile();
        gnusocial_profile_merge($profile);
        foreach ($fields as $field) {
            $action->elementStart('li');
            $fieldname = $field->systemname;
            if ($field->type == 'str') {
                $action->input(
                    $fieldname,
                    $field->title,
                    ($action->arg($fieldname)) ? $action->arg($fieldname) : $profile->$fieldname,
                    $field->description
                );
            } elseif ($field->type == 'text') {
                $action->textarea(
                    $fieldname,
                    $field->title,
                    ($action->arg($fieldname)) ? $action->arg($fieldname) : $profile->$fieldname,
                    $field->description
                );
            }
            $action->elementEnd('li');
        }
        return true;
    }

    public function onEndProfileSaveForm(Action $action): bool
    {
        $fields = GNUsocialProfileExtensionField::allFields();
        $user = common_current_user();
        $profile = $user->getProfile();
        foreach ($fields as $field) {
            $val = $action->trimmed($field->systemname);

            $response = new GNUsocialProfileExtensionResponse();
            $response->profile_id = $profile->id;
            $response->extension_id = $field->id;

            if ($response->find()) {
                $response->fetch();
                $response->value = $val;
                if ($response->validate()) {
                    if (empty($val)) {
                        $response->delete();
                    } else {
                        $response->update();
                    }
                }
            } else {
                $response->value = $val;
                $response->insert();
            }
        }
        return true;
    }*/

    public function onEndShowStyles(Action $action): bool
    {
        $action->cssLink('/plugins/ExtendedProfile/css/profiledetail.css');
        return true;
    }

    public function onEndShowScripts(Action $action): bool
    {
        $action->script('plugins/ExtendedProfile/js/profiledetail.js');
        return true;
    }

    public function onEndAdminPanelNav(AdminPanelNav $nav): bool
    {
        if (AdminPanelAction::canAdmin('profilefields')) {
            $action_name = $nav->action->trimmed('action');

            $nav->out->menuItem(
                common_local_url('profilefieldsAdminPanel'),
                _m('Profile Fields'),
                _m('Custom profile fields'),
                $action_name == 'profilefieldsAdminPanel',
                'nav_profilefields_admin_panel'
            );
        }

        return true;
    }
}


code:
The ExtendedProfile plugin adds additional profile fields such as:

* Phone
* IM
* Website
* Work experience
* Education

And allows administrators to define additional profile fields for the
users of a GNU social installation.

Installation
============
add

    addPlugin('ExtendedProfile');
    $config['admin']['panels'][] = 'profilefields';

to the bottom of your config.php

Note: This plugin is enabled by default on private instances.

Settings
========
none


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('GNUSOCIAL')) {
    exit(1);
}

class ProfileDetailAction extends ShowstreamAction
{
    public function isReadOnly($args)
    {
        return true;
    }

    public function title()
    {
        return $this->target->getFancyName();
    }

    public function showStylesheets()
    {
        parent::showStylesheets();
        $this->cssLink('plugins/ExtendedProfile/css/profiledetail.css');
        return true;
    }

    public function showContent()
    {
        $cur = common_current_user();
        if ($this->scoped instanceof Profile && $this->scoped->sameAs($this->target)) {
            $this->elementStart('div', 'entity_actions');
            $this->elementStart('ul');
            $this->elementStart('li', 'entity_edit');
            $this->element(
                'a',
                array('href' => common_local_url('profiledetailsettings'),
                    // TRANS: Link title for link on user profile.
                    'title' => _m('Edit extended profile settings')),
                // TRANS: Link text for link on user profile.
                _m('Edit')
            );
            $this->elementEnd('li');
            $this->elementEnd('ul');
            $this->elementEnd('div');
        }

        $widget = new ExtendedProfileWidget($this, $this->target);
        $widget->show();
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('GNUSOCIAL')) {
    exit(1);
}

class ProfileDetailSettingsAction extends ProfileSettingsAction
{
    /**
     * Title of the page
     *
     * @return string Title of the page
     */
    public function title()
    {
        // TRANS: Title for extended profile settings.
        // TRANS: %%site.name%% is the name of the site.
        return _m('Extended profile settings');
    }

    protected function doPost()
    {
        if ($this->arg('save')) {
            return $this->saveDetails();
        }

        // TRANS: Message given submitting a form with an unknown action.
        throw new ClientException(_m('Unexpected form submission.'));
    }

    public function showContent()
    {
        $widget = new ExtendedProfileWidget(
            $this,
            $this->scoped,
            ExtendedProfileWidget::EDITABLE
        );
        $widget->show();
    }

    public function saveDetails()
    {
        common_debug(var_export($_POST, true));

        $this->saveStandardProfileDetails();

        $simpleFieldNames = ['title', 'spouse', 'kids', 'manager'];
        $dateFieldNames = ['birthday'];

        foreach ($simpleFieldNames as $name) {
            $value = $this->trimmed('extprofile-' . $name);
            $this->saveField($name, $value);
        }

        foreach ($dateFieldNames as $name) {
            $value = $this->trimmed('extprofile-' . $name);
            $date_val = $this->parseDate($name, $value);
            $this->saveField($name, null, null, null, $date_val);
        }

        $this->savePhoneNumbers();
        $this->saveIms();
        $this->saveWebsites();
        $this->saveExperiences();
        $this->saveEducations();
        $this->saveCustomFields($this);

        // TRANS: Success message after saving extended profile details.
        return _m('Details saved.');
    }

    public function parseDate($fieldname, $datestr, $required = false)
    {
        if (empty($datestr)) {
            if ($required) {
                $msg = sprintf(
                // TRANS: Exception thrown when no date was entered in a required date field.
                // TRANS: %s is the field name.
                    _m('You must supply a date for "%s".'),
                    $fieldname
                );
                throw new Exception($msg);
            }
        } else {
            $ts = strtotime($datestr);
            if ($ts === false) {
                throw new Exception(
                    sprintf(
                    // TRANS: Exception thrown on incorrect data input.
                    // TRANS: %1$s is a field name, %2$s is the incorrect input.
                        _m('Invalid date entered for "%1$s": %2$s.'),
                        $fieldname,
                        $ts
                    )
                );
            }
            return common_sql_date($ts);
        }
        return null;
    }

    public function savePhoneNumbers()
    {
        common_debug('save phone numbers');
        $phones = $this->findPhoneNumbers();
        $this->removeAll('phone');
        $i = 0;
        foreach ($phones as $phone) {
            if (!empty($phone['value'])) {
                ++$i;
                $this->saveField(
                    'phone',
                    $phone['value'],
                    $phone['rel'],
                    $i
                );
            }
        }
    }

    public function findPhoneNumbers()
    {

        // Form vals look like this:
        // 'extprofile-phone-1' => '11332',
        // 'extprofile-phone-1-rel' => 'mobile',

        $phones = $this->sliceParams('phone', 2);
        $phone_array = [];

        foreach ($phones as $phone) {
            list($number, $rel) = array_values($phone);
            $phone_array[] = [
                'value' => $number,
                'rel' => $rel
            ];
        }

        return $phone_array;
    }

    public function findIms()
    {

        //  Form vals look like this:
        // 'extprofile-im-0' => 'jed',
        // 'extprofile-im-0-rel' => 'yahoo',

        $ims = $this->sliceParams('im', 2);
        $imArray = array();

        foreach ($ims as $im) {
            list($id, $rel) = array_values($im);
            $imArray[] = [
                'value' => $id,
                'rel' => $rel
            ];
        }

        return $imArray;
    }

    public function saveIms()
    {
        common_debug('save ims');
        $ims = $this->findIms();
        $this->removeAll('im');
        $i = 0;
        foreach ($ims as $im) {
            if (!empty($im['value'])) {
                ++$i;
                $this->saveField(
                    'im',
                    $im['value'],
                    $im['rel'],
                    $i
                );
            }
        }
    }

    public function findWebsites()
    {

        //  Form vals look like this:

        $sites = $this->sliceParams('website', 2);
        $wsArray = array();

        foreach ($sites as $site) {
            list($id, $rel) = array_values($site);
            $wsArray[] = array(
                'value' => $id,
                'rel' => $rel
            );
        }

        return $wsArray;
    }

    public function saveWebsites()
    {
        common_debug('save websites');
        $sites = $this->findWebsites();
        $this->removeAll('website');
        $i = 0;
        foreach ($sites as $site) {
            if (!empty($site['value']) && !common_valid_http_url($site['value'])) {
                // TRANS: Exception thrown when entering an invalid URL.
                // TRANS: %s is the invalid URL.
                throw new Exception(sprintf(_m('Invalid URL: %s.'), $site['value']));
            }

            if (!empty($site['value'])) {
                ++$i;
                $this->saveField(
                    'website',
                    $site['value'],
                    $site['rel'],
                    $i
                );
            }
        }
    }

    public function findExperiences()
    {

        // Form vals look like this:
        // 'extprofile-experience-0'         => 'Bozotronix',
        // 'extprofile-experience-0-current' => 'true'
        // 'extprofile-experience-0-start'   => '1/5/10',
        // 'extprofile-experience-0-end'     => '2/3/11',

        $experiences = $this->sliceParams('experience', 4);
        $expArray = array();

        foreach ($experiences as $exp) {
            if (count($exp) === 4) {
                [$company, $current, $end, $start] = array_values($exp);
            } else {
                $end = null;
                [$company, $current, $start] = array_values($exp);
            }
            if (!empty($company)) {
                $expArray[] = array(
                    'company' => $company,
                    'start' => $this->parseDate('Start', $start, true),
                    'end' => ($current == 'false') ? $this->parseDate('End', $end, true) : null,
                    'current' => ($current == 'false') ? false : true
                );
            }
        }

        return $expArray;
    }

    public function saveExperiences()
    {
        common_debug('save experiences');
        $experiences = $this->findExperiences();

        $this->removeAll('company');
        $this->removeAll('start');
        $this->removeAll('end'); // also stores 'current'

        $i = 0;
        foreach ($experiences as $experience) {
            if (!empty($experience['company'])) {
                ++$i;
                $this->saveField(
                    'company',
                    $experience['company'],
                    null,
                    $i
                );

                $this->saveField(
                    'start',
                    null,
                    null,
                    $i,
                    $experience['start']
                );

                // Save "current" employer indicator in rel
                if ($experience['current']) {
                    $this->saveField(
                        'end',
                        null,
                        'current', // rel
                        $i
                    );
                } else {
                    $this->saveField(
                        'end',
                        null,
                        null,
                        $i,
                        $experience['end']
                    );
                }
            }
        }
    }

    public function findEducations()
    {

        // Form vals look like this:
        // 'extprofile-education-0-school' => 'Pigdog',
        // 'extprofile-education-0-degree' => 'BA',
        // 'extprofile-education-0-description' => 'Blar',
        // 'extprofile-education-0-start' => '05/22/99',
        // 'extprofile-education-0-end' => '05/22/05',

        $edus = $this->sliceParams('education', 5);
        $eduArray = array();

        foreach ($edus as $edu) {
            list($school, $degree, $description, $end, $start) = array_values($edu);
            if (!empty($school)) {
                $eduArray[] = array(
                    'school' => $school,
                    'degree' => $degree,
                    'description' => $description,
                    'start' => $this->parseDate('Start', $start, true),
                    'end' => $this->parseDate('End', $end, true)
                );
            }
        }

        return $eduArray;
    }

    public function saveEducations()
    {
        common_debug('save educations');
        $edus = $this->findEducations();
        common_debug(var_export($edus, true));

        $this->removeAll('school');
        $this->removeAll('degree');
        $this->removeAll('degree_descr');
        $this->removeAll('school_start');
        $this->removeAll('school_end');

        $i = 0;
        foreach ($edus as $edu) {
            if (!empty($edu['school'])) {
                ++$i;
                $this->saveField(
                    'school',
                    $edu['school'],
                    null,
                    $i
                );
                $this->saveField(
                    'degree',
                    $edu['degree'],
                    null,
                    $i
                );
                $this->saveField(
                    'degree_descr',
                    $edu['description'],
                    null,
                    $i
                );
                $this->saveField(
                    'school_start',
                    null,
                    null,
                    $i,
                    $edu['start']
                );

                $this->saveField(
                    'school_end',
                    null,
                    null,
                    $i,
                    $edu['end']
                );
            }
        }
    }

    public function arraySplit($array, $pieces)
    {
        if ($pieces < 2) {
            return array($array);
        }

        $newCount = ceil(count($array) / $pieces);
        $a = array_slice($array, 0, $newCount);
        $b = $this->arraySplit(array_slice($array, $newCount), $pieces - 1);

        return array_merge(array($a), $b);
    }

    public function findMultiParams($type)
    {
        $formVals = array();
        $target = $type;
        foreach ($_POST as $key => $val) {
            if (strrpos('extprofile-' . $key, $target) !== false) {
                $formVals[$key] = $val;
            }
        }
        return $formVals;
    }

    public function sliceParams($key, $size)
    {
        $slice = array();
        $params = $this->findMultiParams($key);
        ksort($params);
        $slice = $this->arraySplit($params, sizeof($params) / $size);
        return $slice;
    }

    /**
     * Save an extended profile field as a Profile_detail
     *
     * @param string $name field name
     * @param string $value field value
     * @param string|null $rel field rel (type)
     * @param int|null $index index (fields can have multiple values)
     * @param string|null $date date related date
     * @throws ServerException
     */
    public function saveField(string $name, ?string $value, ?string $rel = null, ?int $index = null, ?string $date = null)
    {
        common_debug('save ' . $name);
        $detail = new Profile_detail();

        $detail->profile_id = $this->scoped->getID();
        $detail->field_name = $name;
        $detail->value_index = $index;

        if ($detail->find(true)) {
            // Found an existing Profile Detail, let's update it!
            $orig = clone($detail);

            $detail->field_value = $value ?? $detail->sqlValue('NULL');
            $detail->rel = $rel ?? $detail->sqlValue('NULL');
            $detail->date = $date ?? $detail->sqlValue('NULL');

            if (!$detail->update($orig)) {
                common_log_db_error($detail, 'UPDATE', __FILE__);
                // TRANS: Server error displayed when a field could not be saved in the database.
                throw new ServerException(_m('Could not save profile details.'));
            }
        } elseif (!empty($rel) || !empty($value) || !empty($date)) {
            // It's the first time, let's insert, if there is anything to insert.
            $detail->rel = $rel;
            $detail->field_value = $value;
            $detail->date = $date;
            $detail->created = common_sql_now();

            if (!$detail->insert()) {
                common_log_db_error($detail, 'INSERT', __FILE__);
                // TRANS: Server error displayed when a field could not be saved in the database.
                throw new ServerException(_m('Could not save profile details.'));
            }
        }

        $detail->free();
    }

    public function removeAll($name)
    {
        $detail = new Profile_detail();
        $detail->profile_id = $this->scoped->getID();
        $detail->field_name = $name;
        $detail->delete();
        $detail->free();
    }

    /**
     * Save fields that should be stored in the main profile object
     *
     * XXX: There's a lot of dupe code here from ProfileSettingsAction.
     *      Do not want.
     */
    public function saveStandardProfileDetails()
    {
        $fullname = $this->trimmed('extprofile-fullname');
        $location = $this->trimmed('extprofile-location');
        $tagstring = $this->trimmed('extprofile-tags');
        $bio = $this->trimmed('extprofile-bio');

        if ($tagstring) {
            $tags = array_map(
                'common_canonical_tag',
                preg_split('/[\s,]+/', $tagstring)
            );
        } else {
            $tags = [];
        }

        foreach ($tags as $tag) {
            if (!common_valid_profile_tag($tag)) {
                // TRANS: Validation error in form for profile settings.
                // TRANS: %s is an invalid tag.
                throw new Exception(sprintf(_m('Invalid tag: "%s".'), $tag));
            }
        }

        $oldTags = Profile_tag::getSelfTagsArray($this->scoped);
        $newTags = array_diff($tags, $oldTags);

        if ($fullname != $this->scoped->getFullname()
            || $location != $this->scoped->location
            || !empty($newTags)
            || $bio != $this->scoped->getDescription()) {
            $orig = clone($this->scoped);

            // Skipping nickname change here until we add logic for when the site allows it or not
            // old Profilesettings will still let us do that.

            $this->scoped->fullname = $fullname;
            $this->scoped->bio = $bio;
            $this->scoped->location = $location;

            $loc = Location::fromName($location);

            if (empty($loc)) {
                $this->scoped->lat = null;
                $this->scoped->lon = null;
                $this->scoped->location_id = null;
                $this->scoped->location_ns = null;
            } else {
                $this->scoped->lat = $loc->lat;
                $this->scoped->lon = $loc->lon;
                $this->scoped->location_id = $loc->location_id;
                $this->scoped->location_ns = $loc->location_ns;
            }

            $result = $this->scoped->update($orig);

            if ($result === false) {
                common_log_db_error($this->scoped, 'UPDATE', __FILE__);
                // TRANS: Server error thrown when user profile settings could not be saved.
                throw new ServerException(_m('Could not save profile.'));
            }

            // Set the user tags
            $result = Profile_tag::setSelfTags($this->scoped, $tags);

            Event::handle('EndProfileSaveForm', array($this));
        }
    }

    private function saveCustomFields($action)
    {
        common_debug('save custom fields');
        $fields = GNUsocialProfileExtensionField::allFields();
        $user = common_current_user();
        $profile = $user->getProfile();
        foreach ($fields as $field) {
            $val = $action->trimmed('extprofile-' . $field->systemname);

            if (empty($val)) {
                continue;
            }

            $response = new GNUsocialProfileExtensionResponse();
            $response->profile_id = $profile->id;
            $response->extension_id = $field->id;

            if ($response->find()) {
                $response->fetch();
                $response->value = $val;
                if ($response->validate()) {
                    if (empty($val)) {
                        $response->delete();
                    } else {
                        $response->update();
                    }
                }
            } else {
                $response->value = $val;
                $response->insert();
            }
        }
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social 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.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Allows administrators to define additional profile fields for the users of a GNU social installation.
 *
 * @category  Widget
 * @package   GNU social
 * @author    Max Shinn <trombonechamp@gmail.com>
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2011-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

class ProfilefieldsAdminPanelAction extends AdminPanelAction
{
    /**
     * Title of the page
     *
     * @return string Title of the page
     */
    public function title(): string
    {
        return _m('Profile fields');
    }

    /**
     * Instructions for use
     *
     * @return string instructions for use
     */
    public function getInstructions(): string
    {
        return _m('GNU Social custom profile fields');
    }

    public function showForm(): void
    {
        $form = new ProfilefieldsAdminForm($this);
        $form->show();
    }

    protected function saveField(): void
    {
        $field = GNUsocialProfileExtensionField::getKV('id', $this->trimmed('id'));
        if (!$field) {
            $field = new GNUsocialProfileExtensionField();
            $field->systemname = $this->trimmed('systemname');
            if (!gnusocial_field_systemname_validate($field->systemname)) {
                $this->clientError(_m('Internal system name must be unique and consist of only alphanumeric characters!'));
            }
        }
        $field->title = $this->trimmed('title');
        $field->description = $this->trimmed('description');
        $field->type = $this->trimmed('type');
        if ($field->id) {
            if ($field->validate()) {
                $field->update();
            } else {
                $this->clientError(_m('There was an error with the field data.'));
            }
        } else {
            $field->insert();
        }
    }

    protected function removeField(): void
    {
        // Grab field
        $field = GNUsocialProfileExtensionField::getKV('id', $this->trimmed('id'));
        if (!$field) {
            $this->clientError(_m('Field not found.'));
        }

        // Delete responses to this field
        $responses = new GNUsocialProfileExtensionResponse();
        $responses->extension_id = $field->id;
        $responses->find();
        $responses = $responses->fetchAll();
        foreach ($responses as $response) {
            $response->delete();
        }

        // Delete field
        $field->delete();
    }

    public function saveSettings()
    {
        if ($this->arg('save')) {
            return $this->saveField();
        } elseif ($this->arg('remove')) {
            return $this->removeField();
        }

        // TRANS: Message given submitting a form with an unknown action in e-mail settings.
        throw new ClientException(_('Unexpected form submission.'));
    }
}

class ProfilefieldsAdminForm extends AdminForm
{
    public function id(): string
    {
        return 'form_profilefields_admin_panel';
    }

    public function formClass(): string
    {
        return 'form_settings';
    }

    public function action(): string
    {
        return common_local_url('profilefieldsAdminPanel');
    }

    public function formData(): void
    {
        $title = null;
        $description = null;
        $type = null;
        $systemname = null;
        $id = null;
        $fieldsettitle = _m("New Profile Field");
        // Edit a field
        if ($this->out->trimmed('edit')) {
            $field = GNUsocialProfileExtensionField::getKV('id', $this->out->trimmed('edit'));
            $title = $field->title;
            $description = $field->description;
            $type = $field->type;
            $systemname = $field->systemname;
            $this->out->hidden('id', $field->id, 'id');
            $fieldsettitle = _m("Edit Profile Field");
        } // Don't show the list of all fields when editing one
        else {
            $this->out->elementStart('fieldset');
            $this->out->element('legend', null, _m('Existing Custom Profile Fields'));
            $this->out->elementStart('ul', 'form_data');
            $fields = GNUsocialProfileExtensionField::allFields();
            foreach ($fields as $field) {
                $this->li();
                $this->out->elementStart('div');
                $this->out->element(
                    'a',
                    ['href' => common_local_url('profilefieldsAdminPanel').'?edit=' . $field->id],
                    $field->title
                );
                $this->out->text(' (' . $field->type . '): ' . $field->description);
                $this->out->elementEnd('div');
                $this->unli();
            }
            $this->out->elementEnd('ul');
            $this->out->elementEnd('fieldset');
        }

        // New fields
        $this->out->elementStart('fieldset');
        $this->out->element('legend', null, $fieldsettitle);
        $this->out->elementStart('ul', 'form_data');

        $this->li();
        $this->out->input(
            'title',
            _m('Title'),
            $title,
            _m('The title of the field')
        );
        $this->unli();
        $this->li();
        $this->out->input(
            'systemname',
            _m('Internal name'),
            $systemname,
            _m('The alphanumeric name used internally for this field.  Also the key used for federation (e.g. ActivityPub and OStatus) user info.')
        );
        $this->unli();
        $this->li();
        $this->out->input(
            'description',
            _m('Description'),
            $description,
            _m('An optional more detailed description of the field')
        );
        $this->unli();
        $this->li();
        $this->out->dropdown(
            'type',
            _m('Type'),
            array('text' => _m("Text"),
                'str' => _m("String")),
            _m('The type of the datafield'),
            false,
            $type
        );
        $this->unli();
        $this->out->elementEnd('ul');
        $this->out->elementEnd('fieldset');
    }

    /**
     * Action elements
     *
     * @return void
     * @throws Exception
     */
    public function formActions(): void
    {
        $this->out->submit('save', _m('BUTTON', 'Save'), 'submit', null, _m('Save field'));
        if ($this->out->trimmed('edit')) {
            $this->out->submit('remove', _m('BUTTON', 'Remove'), 'submit', null, _m('Remove field'));
        }
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social 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.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Action for showing Twitter-like JSON search results
 *
 * @category  Search
 * @package   GNUsocial
 * @author    Zach Copley <zach@status.net>
 * @copyright 2011 StatusNet, Inc.
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

class UserautocompleteAction extends Action
{
    public $query;

    /**
     * Initialization.
     *
     * @param array $args Web and URL arguments
     *
     * @return boolean true if nothing goes wrong
     * @throws ClientException
     */
    public function prepare(array $args = [])
    {
        parent::prepare($args);
        $this->query = $this->trimmed('term');
        return true;
    }

    /**
     * Handle a request
     *
     * @return void
     */
    public function handle()
    {
        parent::handle();
        $this->showResults();
    }

    /**
     * Search for users matching the query and spit the results out
     * as a quick-n-dirty JSON document
     *
     * @return void
     * @throws ServerException
     */
    public function showResults()
    {
        $people = array();

        $profile = new Profile();

        $search_engine = $profile->getSearchEngine('profile');
        $search_engine->set_sort_mode('nickname_desc');
        $search_engine->limit(0, 10);
        $search_engine->query(strtolower($this->query . '*'));

        $cnt = $profile->find();

        if ($cnt > 0) {
            $sql = 'SELECT profile.* FROM profile, user WHERE profile.id = user.id '
                . ' AND LEFT(LOWER(profile.nickname), '
                . strlen($this->query)
                . ') = \'%s\' '
                . ' LIMIT 10';

            $profile->query(sprintf($sql, $this->query));
        }

        while ($profile->fetch()) {
            $people[] = $profile->nickname;
        }

        header('Content-Type: application/json; charset=utf-8');
        print json_encode($people);
    }

    /**
     * Do we need to write to the database?
     *
     * @param $args
     * @return boolean true
     */
    public function isReadOnly($args)
    {
        return true;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social 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.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Allows administrators to define additional profile fields for the users of a GNU social installation.
 *
 * @category  Widget
 * @package   GNU social
 * @author    Max Shinn <trombonechamp@gmail.com>
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2011-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

class GNUsocialProfileExtensionField extends Managed_DataObject
{
    public $__table = 'gnusocialprofileextensionfield';
    public $id;          // int(11)
    public $systemname;  // varchar(64)
    public $title;       // varchar(191)   not 255 because utf8mb4 takes more space
    public $description; // text
    public $type;        // varchar(191)   not 255 because utf8mb4 takes more space
    public $created;     // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;    // datetime()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef(): array
    {
        return [
            'fields' => [
                'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Unique ID for extension field'],
                'systemname' => ['type' => 'varchar', 'not null' => true, 'length' => 64, 'description' => 'field systemname'],
                'title' => ['type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'field title'],
                'description' => ['type' => 'text', 'not null' => true, 'description' => 'field description'],
                'type' => ['type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'field type'],
                'created' => ['type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'],
                'modified' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
            ],
            'primary key' => ['id'],
            'indexes' => [
                'gnusocialprofileextensionfield_title_idx' => ['title'],
            ],
        ];
    }

    public static function newField($title, $description = null, $type = 'str', $system_name = null): GNUsocialProfileExtensionField
    {
        $field = new GNUsocialProfileExtensionField();
        $field->title = $title;
        $field->description = $description;
        $field->type = $type;
        if (empty($system_name)) {
            $field->systemname = 'field' . $field->id;
        } else {
            $field->systemname = $system_name;
        }

        $field->id = $field->insert();
        if (!$field->id) {
            common_log_db_error($field, 'INSERT', __FILE__);
            throw new ServerException(_m('Error creating new field.'));
        }
        return $field;
    }

    public static function allFields(): array
    {
        $field = new GNUsocialProfileExtensionField();
        $fields = [];
        if ($field->find()) {
            while ($field->fetch()) {
                $fields[] = clone($field);
            }
        }
        return $fields;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social 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.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Allows administrators to define additional profile fields for the users of a GNU social installation.
 *
 * @category  Widget
 * @package   GNU social
 * @author    Max Shinn <trombonechamp@gmail.com>
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2011-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

class GNUsocialProfileExtensionResponse extends Managed_DataObject
{
    public $__table = 'gnusocialprofileextensionresponse';
    public $id;           // int(11)
    public $extension_id; // int(11)
    public $profile_id;   // int(11)
    public $value;        // text
    public $created;      // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;     // datetime()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef(): array
    {
        return [
            'fields' => [
                'id' => ['type' => 'serial', 'not null' => true, 'description' => 'Unique ID for extension response'],
                'extension_id' => ['type' => 'int', 'not null' => true, 'description' => 'The extension field ID'],
                'profile_id' => ['type' => 'int', 'not null' => true, 'description' => 'Profile id that made the response'],
                'value' => ['type' => 'text', 'not null' => true, 'description' => 'response entry'],
                'created' => ['type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'],
                'modified' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
            ],
            'primary key' => ['id'],
            // Syntax: foreign_key_name => [remote_table, local_key => remote_key]]
            'foreign keys' => [
                'gnusocialprofileextensionresponse_profile_id_fkey' => ['profile', ['profile_id' => 'id']],
                'gnusocialprofileextensionresponse_extension_id_fkey' => ['gnusocialprofileextensionfield', ['extension_id' => 'id']],
            ],
            'indexes' => [
                'gnusocialprofileextensionresponse_extension_id_idx' => ['extension_id'],
            ],
        ];
    }

    public static function newResponse($extension_id, $profile_id, $value): GNUsocialProfileExtensionResponse
    {
        $response = new GNUsocialProfileExtensionResponse();
        $response->extension_id = $extension_id;
        $response->profile_id = $profile_id;
        $response->value = $value;

        $response->id = $response->insert();
        if (!$response->id) {
            common_log_db_error($response, 'INSERT', __FILE__);
            throw new ServerException(_m('Error creating new response.'));
        }
        return $response;
    }

    public static function findResponsesByProfile($id): array
    {
        $extf = 'gnusocialprofileextensionfield';
        $extr = 'gnusocialprofileextensionresponse';
        $sql = "SELECT $extr.*, $extf.title, $extf.description, $extf.type, $extf.systemname FROM $extr JOIN $extf ON $extr.extension_id=$extf.id WHERE $extr.profile_id = $id";
        $response = new GNUsocialProfileExtensionResponse();
        $response->query($sql);
        $responses = [];

        while ($response->fetch()) {
            $responses[] = clone($response);
        }

        return $responses;
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * DataObject class to store extended profile fields. Allows for storing
 * multiple values per a "field_name" (field_name property is not unique).
 *
 * Example:
 *
 *     Jed's Phone Numbers
 *     home  : 510-384-1992
 *     mobile: 510-719-1139
 *     work  : 415-231-1121
 *
 * We can store these phone numbers in a "field" represented by three
 * Profile_detail objects, each named 'phone_number' like this:
 *
 *     $phone1 = new Profile_detail();
 *     $phone1->field_name  = 'phone_number';
 *     $phone1->rel         = 'home';
 *     $phone1->field_value = '510-384-1992';
 *     $phone1->value_index = 1;
 *
 *     $phone1 = new Profile_detail();
 *     $phone1->field_name  = 'phone_number';
 *     $phone1->rel         = 'mobile';
 *     $phone1->field_value = '510-719-1139';
 *     $phone1->value_index = 2;
 *
 *     $phone1 = new Profile_detail();
 *     $phone1->field_name  = 'phone_number';
 *     $phone1->rel         = 'work';
 *     $phone1->field_value = '415-231-1121';
 *     $phone1->value_index = 3;
 *
 */
class Profile_detail extends Managed_DataObject
{
    public $__table = 'profile_detail';

    public $id;
    public $profile_id;  // profile this is for
    public $rel;         // detail for some field types; eg "home", "mobile", "work" for phones or "aim", "irc", "xmpp" for IM
    public $field_name;  // name
    public $field_value; // primary text value
    public $value_index; // relative ordering of multiple values in the same field
    public $date;        // related date
    public $ref_profile; // for people types, allows pointing to a known profile in the system
    public $created;
    public $modified;

    public static function schemaDef()
    {
        return [
            // No need for i18n. Table properties.
            'description' => 'Additional profile details for the ExtendedProfile plugin',
            'fields' => [
                'id' => ['type' => 'serial', 'not null' => true],
                'profile_id' => ['type' => 'int', 'not null' => true],
                'field_name' => [
                    'type' => 'varchar',
                    'length' => 16,
                    'not null' => true,
                ],
                'value_index' => ['type' => 'int'],
                'field_value' => ['type' => 'text'],
                'date' => ['type' => 'datetime'],
                'rel' => ['type' => 'varchar', 'length' => 16],
                'rel_profile' => ['type' => 'int'],
                'created' => [
                    'type' => 'datetime',
                    'not null' => true,
                ],
                'modified' => [
                    'type' => 'timestamp',
                    'not null' => true,
                ],
            ],
            'primary key' => ['id'],
            'unique keys' => [
                'profile_detail_profile_id_field_name_value_index' => ['profile_id', 'field_name', 'value_index']
            ],
        ];
    }
}