How to ignore unregistered namespaces in i18next (colons in strings) - i18next

We use i18next in a CMS to power its internationalization features. Since developers can build however they want with the CMS there is opportunity for them to add l10n keys that include colons, including as part of HTML, such as Find more info here.
As has been documented, with default namespace separator settings i18next will think the colon is identifying a namespace/key pair. Since the CMS uses its own namespace (so devs won't accidentally overwrite UI strings), we don't have the option to turn off namespacing completely (with nsSeparator: false).
What I'm looking for is a way for i18next to only recognize registered namespaces as namespaces. So if we tell i18next that the valid namespaces are ['ns1', 'ns2'] and it receives Title: Subtitle, that string will be treated as a key, not a namespace/key pair.
I saw the loadNamespaces method, but that looks to simply register them to the ns option on the instance. Is there a way for i18next to essentially disallow any unregistered namespace?

This is definitely a workaround, but it doesn't feel too hackey, so I was comfortable using it indefinitely. It's been several weeks, so I don't remember if I got this from somewhere else, but it's possible, for the sake of not taking the credit if undeserved.
First, I added the appendNamespaceToMissingKey: true option to the init function. This adds the default namespace to any keys that don't already have a namespace. It also includes an unknown namespace (or something it thinks is a NS) with the "missing key" for parsing.
Then I added the parseMissingKeyHandler option assigned to the following function:
function (key) {
if (key.startsWith(`${this.defaultNS[0]}:`)) {
return key.slice(this.defaultNS[0].length + 1);
} else {
return key;
}
}
Since I didn't have to hard-code the namespace I felt okay with that.
So if a "key" comes in here with the default namespace, which will include all unlocalized strings without colons (e.g., 'Some text'), that namespace is removed and the string continues on normally. Since i18next doesn't have a value for that string, the string is printed as-is.
If an unlocalized string comes in containing a colon (e.g., '🏛', i18next thinks the first part before the colon is a namespace, so the default is not applied. Therefore this colon-ized string is returned from the function the same as it entered. Again, since i18next doesn't have a value for this string, the string is printed as-is. In this case, that includes the part before the colon as well as the colon separator. We end up with the full link HTML, for example.
So in addition to the other options in place, it looks like:
i18next.init({
...otherOptions,
appendNamespaceToMissingKey: true,
parseMissingKeyHandler (key) {
// We include namespaces with unrecognized l10n keys using
// `appendNamespaceToMissingKey: true`. This passes strings containing
// colons that were never meant to be localized through to the UI.
//
// Strings that do not include colons ("Content area") are given the
// default namespace by i18next ("translation," by default). Here we
// check if the key starts with that default namespace, meaning it
// belongs to no other registered namespace, then remove that default
// namespace before passing this through to be processed and displayed.
if (key.startsWith(`${this.defaultNS[0]}:`)) {
return key.slice(this.defaultNS[0].length + 1);
} else {
return key;
}
}
});
I'm including my code comment since you may also want to include something like this to remind yourself later why you include this convoluted handler.

Related

WebStorm Live Template, separate a string of inputs

I want to create a Live Template for createSelector:
export const someSelector = createSelector(getThis, getThat, getSomethingElse, (this, that, somethingElse) =>
$END$
)
I can get it to work pretty well with a single argument (e.g., only getThis which then results in (this) in the arrow function args).
// template text
createSelector($someSelector$, ($variable$) => $END$)
// variables
// expression for "variable":
decapitalize(regularExpression(someSelector, "get", ""))
This works correctly with a single argument as mentioned above, and almost works correctly with multiple arguments, except for the capitalization:
createSelector(getThis, getThat, getSomethingElse, (this, That, SomethingElse) => /* $end$ */)
I tried wrapping that whole thing in camelCase but then of course the commas and spaces are gone.
The issue is clearly that I'm processing the whole string at once so the whole string is run through whatever string formatting function. There doesn't appear to be any way to treat individual instances of "get" separately.
I tried capture groups which I really thought would work:
decapitalize(regularExpression(someSelector, "get(\w+)", "$1"))
But that doesn't replace anything, it just copies the whole thing:
createSelector(getThis, getThat, (getThis, getThat) => )
Is there any way to accomplish this?
UPDATE:
I even learned Groovy script and wrote the following, which works in a groovy playground, but gets in WebStorm gets the same result as my final example above!
groovyScript("return _1.replaceAll(/get(\w+)/) { it[1].uncapitalize() };", someSelector)
This could be done with RegEx .. but Java does not seem to support \l replacement modifier (to be used as \l$1 instead of $1 in your initial regularExpression() code).
Live example (works in PCRE2, e.g. in PHP): https://regex101.com/r/6faVqC/1
Docs on replacement modifiers: https://www.regular-expressions.info/refreplacecase.html
In any case: this whole thing is handled by Java and you are passing RegEx pattern or GrovyScript code inside double quotes. Therefore any \ symbols would need to be escaped.
You need to replace get(\w+) by get(\\w+).
The following seems to work just fine for me here (where someSelector is the Live Template variable):
groovyScript("return _1.replaceAll(/get(\\w+)/) { it[1].uncapitalize() };", someSelector)

How to check if a riot tag exists?

How can I check if a riot tag has already been loaded and compiled (in-browser with script tag), in order to avoid doing it again, programmatically.
In other words, what should I use instead of doesTagExist function in my simplified code, below?
if (!doesTagExist('my-tag')) {
riot.compile('/path/to/my-tag', function() {
riot.mount('dom-node', 'my-tag');
});
} else {
riot.mount('dom-node', 'my-tag');
}
had same problem. After bit of research I think you can't get it directly. Implementation is stored inside __TAG_IMPL which is not accessible from outside. You can however access selector for all implemented tags via riot.util.tags.selectTags(), which returns comma separated list of selectors i.e. datepicker,[data-is="datepicker"].
Oneliner for convenience
riot.util.tags.selectTags().search(/(^|,)my-tag($|,)/g) >= 0
or depending on your purity inclination
riot.util.tags.selectTags().search('"my-tag"')
Note, that first version is future-proof, if riot decides to start using single commas in selector.

Custom CSS attributes while using LESS?

I have been using SASS for a while now, and one thing I really like is how I can use it for my FlashBuilder projects also, namely that is supports custom CSS attributes, like 'embedAsCFF' and 'unicodeRange'.
I'm trying out LESS for the first time, and it will not let me compile to CSS while using these two custom attributes:
embedAsCFF: true;
unicodeRange: U+0021, U+0023-U+0026, U+0028-U+002a, U+002c, U+002e-U+0039, U+0040-U+005d, U+0061-U+007d;
I receive a 'Less Compilation Error: Syntax Error...'
Any LESS users know how I need to add in support for these custom attributes? Thanks in advance.
Update: This issue will be resolved in the release of LESS 1.4.2
Not a Custom Name but a Format Issue
It appears on my experimenting that the issue is really the fact that you are using capital letters in the property names (not that they are custom attributes themselves). Capital letters are apparently not supported by LESS. In other words, these work:
embedascff: true;
embed-as-cff: true;
unicoderange: U+0021; //etc.
unicode-range: U+0021; //etc.
But this does not:
Color: red;
I have not for certain isolated where in the actual LESS code itself this might be fixed (if it can be fixed for the way LESS handles the property rules). I suspect the cause is in the parser.js file lines 1578-1584 (as of this writing), which are:
property: function () {
var name;
if (name = $(/^(\*?-?[_a-z0-9-]+)\s*:/)) {
return name[1];
}
}
This seems to be filtering out allowing for capital letters. I don't know what the consequences would be if that regular expression was changed to allow for them.

Why does Javassist insist on looking for a default annotation value when one is explicitly specified?

I am using Javassist to add and modify annotations on a package-info "class".
In some cases, I need to deal with the following edge case. Someone has (incorrectly) specified an #XmlJavaTypeAdapters annotation on the package-info package, but has not supplied a value attribute (which is defined as being required). So it looks like this:
#XmlJavaTypeAdapters // XXX incorrect; value() is required, but javac has no problem
package com.foobar;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
In Javassist, this comes through slightly oddly.
The javassist.bytecode.annotation.Annotation representing the #XmlJavaTypeAdapters annotation does not have a member value (getMemberValue("value") returns null), as expected.
It is of course possible to add a value() member value, and that is what I've done:
if (adaptersAnnotation.getMemberValue("value") == null) {
final ArrayMemberValue amv = new ArrayMemberValue(new AnnotationMemberValue(constantPool), constantPool);
adaptersAnnotation.addMemberValue("value", amv);
annotationsAttribute.addAnnotation(adaptersAnnotation);
}
In the code snippet above, I've created a new member value to hold an array of annotations, because the value() attribute of #XmlJavaTypeAdapters is an array of #XmlJavaTypeAdapter. I've specified its array type by trying to divine the Zen-like documentation's intent—it seems that if you supply another MemberValue that this MemberValue will somehow serve as the array's type. In my case I want the type of the array to be #XmlJavaTypeAdapter, which is an annotation, so the only kind of MemberValue that seemed appropriate was AnnotationMemberValue. So I've created an empty one of those and set it as the array type.
This works fine as far as it goes, as long as you stay "within" Javassist.
However, something seems to have gone wrong. If I ask Javassist to convert all of its proprietary annotations into genuine Java java.lang.annotation.Annotations, then when I try to access the value() attribute of this #XmlJavaTypeAdapters annotation, Javassist tells me that there is no default value. Huh?
In other words, that's fine—indeed there is not—but I have specified what I had hoped was a zero-length array (that is, the default value shouldn't be used; my explicitly specified zero-length array should be used instead):
final List<Object> annotations = java.util.Arrays.asList(packageInfoClass.getAnnotations());
for (final Object a : annotations) {
System.out.println("*** class annotation: " + a); // OK; one of these is #XmlJavaTypeAdapters
System.out.println(" ...of type: " + a.getClass()); // OK; resolves to XmlJavaTypeAdapters
System.out.println(" ...assignable to java.lang.annotation.Annotation? " + java.lang.annotation.Annotation.class.isInstance(a)); // OK; returns true
if (a instanceof XmlJavaTypeAdapters) {
final XmlJavaTypeAdapters x = (XmlJavaTypeAdapters)a;
System.out.println(" ...value: " + java.util.Arrays.asList(x.value())); // XXX x.value() throws an exception
}
}
So why is Javassist looking for a default value in this case?
My larger issue is of course to handle this (unfortunately somewhat common) case where #XmlJavaTypeAdapters is specified with no further information on it. I need to add a value member value that can hold an array of #XmlJavaTypeAdapter annotations. I can't seem to figure out how to accomplish this with Javassist. As always, all help appreciated.
For posterity, it appears that in this particular case (to avoid a NullPointerException and/or a RuntimeException), you need to do this:
if (adaptersAnnotation.getMemberValue("value") == null) {
final ArrayMemberValue amv = new ArrayMemberValue(constantPool);
amv.setValue(new AnnotationMemberValue[0]);
adaptersAnnotation.addMemberValue("value", amv);
annotationsAttribute.addAnnotation(adaptersAnnotation);
}
Note in particular that I deliberately omit the array type when building the ArrayMemberValue (including one of any kind will result in an exception). Then I explicitly set its value to an empty array of type AnnotationMemberValue. Any other combination here will result in an exception.
Additionally, and very oddly, the last line in that if block is critical. Even though in this particular case the annotation itself was found, and so hence was already present in the AnnotationsAttribute, you must re-add it. If you do not, you will get a RuntimeException complaining about the lack of a default value.
I hope this helps other Javassist hackers.

Use UUID as action parameters in Play Framework

I'd like to use UUID as action parameters. However, unless I use .toString() on the UUID objects when generating the action URL's, Play seems to serialize the object differently; Something like this: referenceId.sequence=-1&referenceId.hashCode=1728064460&referenceId.version=-1&referenceId.variant=-1&referenceId.timestamp=-1&referenceId.node=-1
However, using toString "works", but when I redirect from one action to another by simply invoking the method directly, there's no way I can call toString, as the method expects a UUID. Therefore it gives me the representation shown above.
Is there any way I can intersect the serialization of a certain type?
aren't you able to just use string in your action parameter? you know that this string is an UUID, so you can always recreate UUID from it. Maybe this is not the solution for you but that's my first thought. As far as I know play serializes objects like that when passing them trough paremeters.
If this does not work for you try finding something here: http://www.playframework.org/documentation/1.2.4/controllers
I found a way to do this, but right now it means hacking a part of the frameworks code itself.
What you basically need is a TypeBinder for binding the value from the String to the UUID
and a small code change in
play/framework/src/play/data/binding/Unbinder.java
if (!isAsAnnotation) {
// We want to use that one so when redirecting it looks ok. We could as well use the DateBinder.ISO8601 but the url looks terrible
if (Calendar.class.isAssignableFrom(src.getClass())) {
result.put(name, new SimpleDateFormat(I18N.getDateFormat()).format(((Calendar) src).getTime()));
} else {
result.put(name, new SimpleDateFormat(I18N.getDateFormat()).format((Date) src));
}
}
}
//here's the changed code
else if (UUID.class.isAssignableFrom(src.getClass()))
{
result.put(name, src.toString());
}
else {
// this code is responsible for the behavior you're seeing right now
Field[] fields = src.getClass().getDeclaredFields();
for (Field field : fields) {
if ((field.getModifiers() & BeanWrapper.notwritableField) != 0) {
// skip fields that cannot be bound by BeanWrapper
continue;
}
I'm working with the framework authors on a fix for this. will come back later with results.
if you need this urgently, apply the change to the code yourself and rebuild the framework by issuing
ant
in the playframework/framework
directory.