The following does what you want.
I coded it in L3 instead of on the low level David used. The basic idea is still the same, if it starts with something that expands to \UTFviii@two@octets we collect those two octets, else we just use the first character. However, this also uses \text_purify:n on the input to ensure the input to the loop is only text.
Then we simply split at each space (we have to check whether we're done with \quark_if_recursion_tail_stop:n) and get our first character (or the two that make up the UTF8 octet pair).
The macro \firstofwords takes as its first argument the stuff it should put after each first letter of each word, and the second argument is the list of words separated by spaces.
\documentclass{article}
\usepackage[T2A]{fontenc}
\usepackage[russian]{babel}
\makeatletter
\newcommand{\firstof}[1]{\expandafter\checkfirst#1@nil}
\def\checkfirst#1{%
\ifx\UTFviii@two@octets#1%
\expandafter\gettwooctets
\else
\expandafter@car\expandafter#1%
\fi
}
\def\gettwooctets#1#2#3@nil{\UTFviii@two@octets#1#2}
\ExplSyntaxOn
\cs_new:Npn \crosfield_first_of_words:nn #1#2
{ \exp_not:e { __crosfield_first_of_words:e { \text_purify:n {#2} } {#1} } }
\group_begin:
\cs_set:Npn __crosfield_tmp:n #1
{
\cs_new:Npn __crosfield_first_of_words:n ##1 ##2
{
__crosfield_first_of_words_spaces:nw {##2} ##1 #1 % #1 is a space
\q_recursion_tail #1 % #1 is a space
\q_recursion_stop
}
}
__crosfield_tmp:n { ~ }
\group_end:
\cs_generate_variant:Nn __crosfield_first_of_words:n { e }
\makeatletter
\cs_new:Npn __crosfield_first_of_words_spaces:nw #1 #2 ~
{
\quark_if_recursion_tail_stop:n {#2}
\tl_if_head_eq_meaning:oNTF {#2} \UTFviii@two@octets
{ __crosfield_first_of_words_aux:nnw #2 \q_stop }
{ \tl_head:n {#2} }
\exp_not:n {#1}
__crosfield_first_of_words_spaces:nw {#1}
}
\makeatother
\cs_generate_variant:Nn \tl_if_head_eq_meaning:nNTF { o }
% there can't be a \q_stop in the argument as that would've caused an infinite
% loop in \text_purify:n, so this is fine and faster than allowing arbitrary
% contents.
\cs_new:Npn __crosfield_first_of_words_aux:nnw #1#2#3 \q_stop
{ \exp_not:n { #1#2 } }
% define a LaTeX2e name for the above function
\cs_new_eq:NN \firstofwords \crosfield_first_of_words:nn
\ExplSyntaxOff
\begin{document}
\firstof{Vladimir}
\firstof{Владимир}
\firstofwords{.\ }{Vladimir Владимир} example
\firstofwords{.}{foo bar baz}
\end{document}

Edit: If you only need the second argument and want to put a . inbetween the first letters every time you can use the following instead of \cs_new_eq:NN \firstofwords \crosfield_first_of_words:nn:
\newcommand* \firstofwords { \crosfield_first_of_words:nn {.} }
Edit2:
This variant only places the first argument to \firstofwords between elements, but not after the last one.
\documentclass[varwidth,border=3.14]{standalone}
\usepackage[T2A]{fontenc}
\usepackage[russian]{babel}
\makeatletter
\newcommand{\firstof}[1]{\expandafter\checkfirst#1@nil}
\def\checkfirst#1{%
\ifx\UTFviii@two@octets#1%
\expandafter\gettwooctets
\else
\expandafter@car\expandafter#1%
\fi
}
\def\gettwooctets#1#2#3@nil{\UTFviii@two@octets#1#2}
\ExplSyntaxOn
\cs_new:Npn \crosfield_first_of_words:nn #1#2
{ \exp_not:e { __crosfield_first_of_words:e { \text_purify:n {#2} } {#1} } }
\group_begin:
\cs_set:Npn __crosfield_tmp:n #1
{
\cs_new:Npn __crosfield_first_of_words:n ##1 ##2
{
__crosfield_first_of_words_spaces:Nnw \use_none:n {##2} ##1 #1 % #1 is a space
\q_recursion_tail #1 % #1 is a space
\q_recursion_stop
}
}
__crosfield_tmp:n { ~ }
\group_end:
\cs_generate_variant:Nn __crosfield_first_of_words:n { e }
\makeatletter
\cs_new:Npn __crosfield_first_of_words_spaces:Nnw #1 #2 #3 ~
{
\quark_if_recursion_tail_stop:n {#3}
#1 {#2}
\tl_if_head_eq_meaning:oNTF {#3} \UTFviii@two@octets
{ __crosfield_first_of_words_aux:nnw #3 \q_stop }
{ \tl_head:n {#3} }
__crosfield_first_of_words_spaces:Nnw \exp_not:n {#2}
}
\makeatother
\cs_generate_variant:Nn \tl_if_head_eq_meaning:nNTF { o }
% there can't be a \q_stop in the argument as that would've caused an infinite
% loop in \text_purify:n, so this is fine and faster than allowing arbitrary
% contents.
\cs_new:Npn __crosfield_first_of_words_aux:nnw #1#2#3 \q_stop
{ \exp_not:n { #1#2 } }
% define a LaTeX2e name for the above function
\cs_new_eq:NN \firstofwords \crosfield_first_of_words:nn
\ExplSyntaxOff
\begin{document}
\firstof{Vladimir}
\firstof{Владимир}
:\firstofwords{.,}{Vladimir Владимир}.:
\firstofwords{.}{foo bar baz}
\end{document}
If you want to always use .\, between elements and a . after the last one you can use
\newcommand\firstofwords[1]{ \crosfield_first_of_words:nn {.\,} {#1} . }
Instead of the \cs_new_eq:NN-line.
\firstofwordsfunction is not needed. There should always be a dot char and in this sense it does not need to be passed inside the function. – Crosfield Jul 09 '23 at 21:13\crosfield_first_of_words:nn {.\,}command, these characters will be added after each initial, but I need the.\,to be added only between the initials. Only a dot is needed at the end of the result (likeA.\,F.). Is it possible to do this? – Crosfield Jul 10 '23 at 09:53\unskipafter the last one to remove it:\firstofwords{.\,}{Some string here}\unskip. If you don't like to add\unskipfor this I could create some other code, but that needs time (and I don't have much time today) – Skillmon Jul 10 '23 at 11:13\unskiptrick doesn't quite work. It's easier to put a negative space\!after\q_recursion_stop. Although this is of course also just dirty trick. – Crosfield Jul 10 '23 at 13:40\,uses\kernin text mode, so can't be\unskipped. – Skillmon Jul 11 '23 at 16:23