<...> but maybe there's a better solution?

Yes, of course.

E.g. (for Windows x64):

#include %systemInclude

; see "dir /?"
##class(%Net.Remote.Utility).RunCommandViaCPIPE($$$FormatText("dir /A-D /B /S %1",$$$quote("C:\Program Files (x86)\Common Files\InterSystems")),,.con)
; string -> array
##class(%ListOfDataTypes).BuildValueArray($lfs($e(con,1,*-2),$$$NL),.array)
zw array

Refine your version $zv. Perhaps in older versions there were bugs that were then fixed.

Simple test:

Class my.children Extends %Persistent
{

Property p1;

Relationship parent As my.parent [ Cardinality = parent, Inverse = children ];

Method %OnConstructClone(
  object As %RegisteredObject,
  deep As %Boolean = 0,
  ByRef cloned As %String) As %Status [ Private, ServerOnly = 1 ]
{
  i%p1 = object.p1 _ " cloned"
  $$$OK
}
}

Class my.parent Extends %Persistent
{

Property p1;

Relationship children As my.children [ Cardinality = children, Inverse = parent ];

ClassMethod Fill()
{
  ;d ##class(my.parent).Fill()
  
  ..%KillExtent()
  
  i = 1:1:3 {
    = ..%New()
    p.p1 = "parent["_i_"]"
    j = 1:1:2 {
      = ##class(my.children).%New()
      c.p1 = $$$FormatText("children[%1,%2]",i,j)
      p.children.Insert(c)
    }
    p.%Save()
  }
  zw ^my.parentD
  !,"_______",!!
  
  i = 1,3 ..%OpenId(i).%ConstructClone($$$YES).%Save()

  ; or so - the result is the same
  ; f i = 1,3 d ..%OpenId(i).%ConstructClone($$$NO).%Save()

  zw ^my.parentD
}

Method %OnConstructClone(
  object As %RegisteredObject,
  deep As %Boolean = 0,
  ByRef cloned As %String) As %Status [ Private, ServerOnly = 1 ]
{
  i%p1 = object.p1 _ " cloned"
  $$$OK
}
}


USER>$zv
Cache for Windows (x86-64) 2017.2 (Build 672U) Wed May 10 2017 20:43:42 EDT
USER>##class(my.parent).Fill()
^my.parentD=3
^my.parentD(1)=$lb("","parent[1]")
^my.parentD(1,"children",1)=$lb("","children[1,1]")
^my.parentD(1,"children",2)=$lb("","children[1,2]")
^my.parentD(2)=$lb("","parent[2]")
^my.parentD(2,"children",3)=$lb("","children[2,2]")
^my.parentD(2,"children",4)=$lb("","children[2,1]")
^my.parentD(3)=$lb("","parent[3]")
^my.parentD(3,"children",5)=$lb("","children[3,2]")
^my.parentD(3,"children",6)=$lb("","children[3,1]")
 
_______
 
^my.parentD=5
^my.parentD(1)=$lb("","parent[1]")
^my.parentD(1,"children",1)=$lb("","children[1,1]")
^my.parentD(1,"children",2)=$lb("","children[1,2]")
^my.parentD(2)=$lb("","parent[2]")
^my.parentD(2,"children",3)=$lb("","children[2,2]")
^my.parentD(2,"children",4)=$lb("","children[2,1]")
^my.parentD(3)=$lb("","parent[3]")
^my.parentD(3,"children",5)=$lb("","children[3,2]")
^my.parentD(3,"children",6)=$lb("","children[3,1]")
^my.parentD(4)=$lb("","parent[1] cloned")
^my.parentD(4,"children",7)=$lb("","children[1,2] cloned")
^my.parentD(4,"children",8)=$lb("","children[1,1] cloned")
^my.parentD(5)=$lb("","parent[3] cloned")
^my.parentD(5,"children",9)=$lb("","children[3,2] cloned")
^my.parentD(5,"children",10)=$lb("","children[3,1] cloned")
The next question is, what hashing algorithm is Cache using to create the hash?

CRC32

I will need to replicate that in JavaScript, or come up with some other hash approach.

Localization for Client-side Text

I assume $mvv(58) is set by the system when its installed? But can't find any information on that at the moment.

$mvv(58) - Default Language for a session either user defined or CSP based.

PS: Look at the source code %occMessages.inc - there's a lot of tasty there ;) or read the article in my blog, but it's in russian language :(

The only single problem here – there is no direct support for importing from .JSON files even for recent versions of Caché/Ensemble with builtin JSON support in the kernel.

See %Library.DynamicAbstractObject:%FromJSON(), %ZEN.Auxiliary.jsonProvider:%ConvertJSONToObject(), %ZEN.Auxiliary.jsonProvider:%ParseFile(), %ZEN.Auxiliary.jsonProvider:%ParseJSON()

Given:
test.json: {"a":"test","b":[1,2,3,4]}

    file=##class(%Stream.FileCharacter).%New()
    file.Filename="C:\test.json"
    file.TranslateTable="UTF8"
    ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(file,,.obj)
    obj.%Print()
 

Output:

-----------------
a: "test"
b:
  [1] 1
  [2] 2
  [3] 3
  [4] 4

> Can I make a serial class always computed?
Yes, of course.

Class CS.Serial Extends %SerialObject [ NoExtent ]
{
Property Year As %Integer;
Property Month As %Integer;
}

Class CS.Persistent Extends %Persistent
{

Property data As CS.Serial [ SqlComputeCode = {set {*} = ##class(CS.Persistent).dataGetStatic()}, SqlComputed, Transient ];

ClassMethod dataGetStatic() As %List
{
  quit $lb(2017,1)
}
}

Output:
^CS.PersistentD=1
^CS.PersistentD(1)=$lb("")
ID      data
1       $lb(2017,1)
 
1 Rows(s) Affected

It all depends on what you wish to achieve.
For example:

Class CS.Serial Extends %SerialObject
{
Property Year As %Integer;
Property Month As %Integer;

Method %MyGetSerial() As %List [ ServerOnly = 1 ]
{
  q $lb(i%Year,i%Month)
}
}

Class CS.Persistent Extends %Persistent
{
/// Property is triggered computed
Property data1 As CS.Serial [ SqlComputeCode = {set {*} = ..dataGetStatic()}, SqlComputed ];
/// Property is always computed
Property data2 As %List [ SqlComputeCode = {set {*} = ##class(CS.Persistent).dataGetStatic()}, SqlComputed, Transient ];

/// data getter method in SQL context
ClassMethod dataGetStatic() As %List
{
  s s=##class(CS.Serial).%New(), s.Month=1, s.Year=2017
  q s.%MyGetSerial()
  ; or
  q $lb(2017,1)
}
}

---------

select ID,data1,data2,data1_Month,data1_Year from CS.Persistent

ID  data1   data2   data1_Month  data1_Year
1   2017,1  2017,1  1            2017
2   2017,1  2017,1  1            2017
3   2017,1  2017,1  1            2017

You could do the same thing by creating a View with a function in the WHERE clause that called the method to figure out if you should see the row or not.

  1. responsibility lies on developer don't to forget add additional conditions: WHERE, %IGNOREINDEX, etc.
  2. the query becomes more complicated

I think this is a cleaner solution as you do not need to worry about the %RLI becoming available again. 

What prevents force disable %RLI in ^%ZSTART or $SYSTEM.SQL.SetServerInitCode() ?

Important: pre-need string to convert to UTF-8

USER>w ##class(%xsd.hexBinary).LogicalToXSD($system.Encryption.SHAHash(512,$zcvt("тестtest","O","UTF8")))
83B481B1A58FA416F34BFF575F2DCA866C6AB790CF0941BC0F1E7E91D916D22866CB9575C666021B2F8A796B20A484ABB29403853EDEA6173B9A61758060117E
USER>w ##class(%xsd.hexBinary).LogicalToXSD($system.Encryption.SHAHash(512,$zcvt("test","O","UTF8")))
EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF