Skip to content

Instantly share code, notes, and snippets.

@maddyblue
Last active August 22, 2019 04:49
Show Gist options
  • Save maddyblue/5052106 to your computer and use it in GitHub Desktop.
Save maddyblue/5052106 to your computer and use it in GitHub Desktop.
Roslyn- and UglifyJS-based extractors for translation. All files subject to the attached license.
using System.Collections.Generic;
using System.Linq;
using Roslyn.Compilers.CSharp;
using Roslyn.Compilers.Common;
namespace LocalizationExtractor
{
class InvocationExtractor : SyntaxWalker
{
public readonly List<StringInfo> Strings = new List<StringInfo>();
private ISemanticModel model { get; set; }
public InvocationExtractor(ISemanticModel model)
{
this.model = model;
}
private void ProcessThing(dynamic obj, bool multiLine = false)
{
var stringInfo = StringInfo.Create(obj, model);
if (stringInfo != null)
{
stringInfo.MultiLine = multiLine;
Strings.Add(stringInfo);
}
}
private static readonly IEnumerable<string> SINGLE_LINE = new[] { "_s", "_m" };
private static readonly IEnumerable<string> MULTI_LINE = new[] { "_ms" };
public override void VisitInvocationExpression(InvocationExpressionSyntax node)
{
base.VisitInvocationExpression(node);
var callname = node.Expression.GetText().ToString();
if (SINGLE_LINE.Any(callname.EndsWith))
{
ProcessThing(node.ArgumentList);
}
else if (MULTI_LINE.Any(callname.EndsWith))
{
ProcessThing(node.ArgumentList, multiLine: true);
}
}
private const string ERROR_MESSAGE = "ErrorMessage";
public override void VisitAttributeArgument(AttributeArgumentSyntax node)
{
base.VisitAttributeArgument(node);
if (node.NameEquals == null || node.NameEquals.Name.Identifier.ValueText != ERROR_MESSAGE) return;
ProcessThing(node.Expression);
}
}
public class StringInfo
{
public string Text { set; private get; }
public bool MultiLine { get; set; }
public bool Count { get; set; }
/// <summary>
/// The original, untouched text. Calculate the SHA from this.
/// </summary>
public string Orig
{
get
{
return Text;
}
}
/// <summary>
/// A cleaned version of the string: whitespace is trimmed and double spaces collapsed to single. Newlines are removed if MultiLine == true.
/// This is what should be sent to translators.
/// </summary>
public string Clean
{
get
{
var t = Text;
if (!MultiLine)
{
t = t.Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' ');
while(t.Contains(" "))
{
t = t.Replace(" ", " ");
}
}
else
{
t = t.Replace("\r", "\\\r");
}
t = t.Trim();
return t;
}
}
public static StringInfo Create(ArgumentListSyntax args, ISemanticModel model)
{
var first = args.Arguments.FirstOrDefault();
if (first == null || first.Expression.Kind != SyntaxKind.StringLiteralExpression)
{
return null;
}
var stringInfo = new StringInfo
{
Text = first.Expression.GetFirstToken().ValueText,
};
var second = args.Arguments.Skip(1).FirstOrDefault();
if (second != null)
{
stringInfo.Count = HasCount((dynamic)second.Expression, model);
}
return stringInfo;
}
public static StringInfo Create(LiteralExpressionSyntax literal, ISemanticModel model)
{
if (literal == null)
{
return null;
}
return new StringInfo
{
Text = literal.Token.ValueText,
Count = false,
};
}
private const string COUNT = "__count";
public static bool HasCount(AnonymousObjectCreationExpressionSyntax arg, ISemanticModel model)
{
var result = arg.Initializers.Any(x =>
{
if(x.NameEquals != null)
{
return x.NameEquals.Name.Identifier.ValueText == COUNT;
}
return HasCount((dynamic)x.Expression, model);
});
return result;
}
public static bool HasCount(IdentifierNameSyntax arg, ISemanticModel model)
{
var symbols = model.LookupSymbols(arg.Span.Start, name: arg.Identifier.ValueText);
var first = symbols.First();
TypeSymbol type = ((dynamic)first).Type;
return type.GetMembers(COUNT).Any();
}
public static bool HasCount(MemberAccessExpressionSyntax arg, ISemanticModel model)
{
var name = arg.Expression as IdentifierNameSyntax;
if (name != null)
{
return HasCount(name, model);
}
return false;
}
public static bool HasCount(ExpressionSyntax arg, ISemanticModel model)
{
return false;
}
}
}
jsp = require('uglify-js').parser;
util = require('util');
PLURAL_IDENT = '__count';
// Searches the ast for a call to anything in search with the given prefix
// and appends the first argument to results. For example, to find 'Careers._s':
// ast_search(ast, 'Careers', ['_s'], results);
function ast_search(ast, prefix, search, results) {
for (var i = 0; i < ast.length; i++) {
var e = ast[i];
if (util.isArray(e)) {
ast_search(e, prefix, search, results);
}
else if (e == 'call') {
var call = ast[i + 1];
if (call && call[0] == 'dot' && call[1][1] == prefix && search.indexOf(call[2]) >= 0) {
var block = ast[i + 2];
var arg = block[0][1];
if (arg.length > 0 && results.indexOf(arg) < 0) {
if (isValid(arg)) {
var counted = arg.indexOf(PLURAL_IDENT) >= 0;
if (block[1] && block[1][0] === 'object') {
for(var j = 0; j < block[1][1].length; j++) {
if(block[1][1][j][0] == PLURAL_IDENT) {
counted = true;
break;
}
}
}
results.push([arg, counted]);
}
else {
console.log('Invalid string:', arg);
}
}
}
}
}
}
WS_AT_START = /^\s/;
WS_AT_END = /\s$/;
function isValid(s) {
return !WS_AT_START.test(s) && !WS_AT_END.test(s);
}
/* Loop through all your .js files and parse them:
try {
var ast = jsp.parse(code);
}
catch (e) {
results.push('failed to parse');
return;
}
var r = [];
var PREFIX = 'Careers'; // or whatever your global object is so you can use Careers._s("stuff")
ast_search(ast, PREFIX, ['_s'], r);
*/
Copyright 2013 Stack Exchange
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment