What I want:
A language where the "String" data type is as follows:
A rope of "logical characters" (One or more code points, such that they are logically one character. So an accent is combined with the previous character, that sort of thing.)
With the additional "restriction" (read: implementation detail) that within a single node all logical characters must have the same width. (You can, for example, store a single one-byte character in a run of two-byte characters as an overlong-encoded two-byte character, but this is just an optimization.)
Short ropes degenerate to a flat array.
(You have to do a workaround for single code points that encode multiple logical characters. You split them into N parts encoded in the private unicode range or something similar, and when displaying them recombine them if they are in the correct order, otherwise normalize them. Although I'm up in the air about this. Should reverse("st") be "st"? Or "ts"? (That's the single unicode character "st", for those that are confused.))
Ideally, you put character encoding directly within nodes.
That way most things "just work". Running a string through the encoder twice doesn't do anything, as it detects the encoding is the same as the target encoding and doesn't do anything. Reversing a string "just works". Indexing a string is sub-linear time, but gives decent results. (Indexing a string and getting invalid unicode as a result is never fun!) Concatenating strings takes sublinear time even. This works really well with immutable data structures, or quasi-immutable data structures. (there's some tricks with rewriting ropes to take maximal advantage of structure-sharing that preserve the illusion of an immutable data structure without actually being immutable.)
And if you really want you can start doing fancy things like allowing lazy generators within strings, or lazily decompressing / reading data from disk.
To store on disk? Yeah, go with UTF-8. (Or my personal favorite pet encoding: compressed UTF-32.)