I ended up using parameterized macros to replace the generate variable with a string when passed to RLOC. It's quite a workaround, and I most likely would have used Greg's solution had I seen it earlier.
Anyway, in case people are actually interested, the macros :
parameter DIGITS = "9876543210";
`define THOUSANDS(x) (x / 1000)
`define HUNDREDS(x) ((x - (`THOUSANDS(x) * 1000)) / 100)
`define TENS(x) ((x - (`THOUSANDS(x) * 1000) - (`HUNDREDS(x) * 100)) / 10)
`define ONES(x) (x - (`THOUSANDS(x) * 1000) - (`HUNDREDS(x) * 100) - (`TENS(x) * 10))
`define TO_STRING(x) (DIGITS[((8 * (x + 1)) - 1) : (8 * x)])
`define VAR_TO_STRING(x) ({`TO_STRING(`THOUSANDS(x)), `TO_STRING(`HUNDREDS(x)), `TO_STRING(`TENS(x)), `TO_STRING(`ONES(x))})
The macros THOUSANDS(), HUNDREDS(), TENS(), and ONES() return the number of thousands, hundreds, tens, and ones found in x. These macros should always return 1 digit values.
TO_STRING() takes some 1 digit value and "converts" it to a string by returning a portion of parameter DIGITS.
VAR_TO_STRING() uses all of the above macros in conjunction to convert any 4 digit integer into its string equivalent. The code in the question would then be replaced by :
genvar i;
generate
for (i = 0; i < DATA_WIDTH; i = i + 1)
begin : LATCH
(* RLOC = {"X0Y", `VAR_TO_STRING(i)} *)
latch inst_latch (
.d (data_in[i]),
.gate (gate),
.reset (reset),
.q (data_out[i])
);
end
endgenerate