Skip to content

Instantly share code, notes, and snippets.

@huguesbr
Last active July 20, 2017 17:32
Show Gist options
  • Save huguesbr/375730d567020da83836483074a67ff9 to your computer and use it in GitHub Desktop.
Save huguesbr/375730d567020da83836483074a67ff9 to your computer and use it in GitHub Desktop.
Automatically replace annotated String (see format below) in Swift with corresponding tr(...) calls for https://github.com/AliSoftware/SwiftGen. Copy both into the project and call 'sh replaceAnnotatedLocalizeString.sh'
#!/usr/bin/env ruby
# take annotated string and transforms it to the tr(...) format as well as creating the localizable.strings entry
# arguments
localizable=ARGV[0]
filename=ARGV[1]
def makeFirstLetterUpper(string)
string[0].upcase + string[1..-1]
end
def namify(string)
# take a string
# return: transform it to a localizing key
# 1Hello,world -> _1HelloWorld
# treat all whitespace, "" and , like a word split and uppercase next letwordter
string = string.split(/\s+/).map { |a| makeFirstLetterUpper(a) }.join("")
string = string.split(",").map { |a| makeFirstLetterUpper(a) }.join("")
string = string.split("\"").map { |a| makeFirstLetterUpper(a) }.join("")
# handle _ like . (except first one)
string = string.gsub("(.)\_", '\1.')
# handle any others characters like a word split, strip it and uppercase next word
string = string.split(/[^a-zA-Z0-9._]/).map { |a| makeFirstLetterUpper(a) if a.length > 0 }.join("")
# prefix first digit with _
string = string.gsub(/^([0-9])/, '_\1')
end
def variabilize(string, types)
# take a string and some eventual types or variables
# return: the string with the named argument replaced by
# "Hello world" -> "Hello world", []
# "Hello \(world)" -> "Hello %@", [world]
variables = string.scan(/\\\(([^)]+)\)/)
if variables.length > 0
# string contain variable
variables = variables.map { |k| k[0] }
for t in types
string = string.sub(/\\\([^)]+\)/, t)
end
end
[string, variables]
end
# check string
match = localizable.scan(/"(.*)" \/\/ !tr(: (.*))?/i)
# "String" // tr: Key
# "String" // tr
if match.length > 0
s = match[0][0]
# determine key
t = []
custom_key = false
if match[0][2] == nil
# "Hello Wolrd" // tr
# k = HelloWorld
k = tr = namify(s)
else
# "Hello \(count) \(world)" // tr: HelloWorld(%d, %@)
# k = HelloWorld
# v = %d, %@
custom_key = true
kv = match[0][2]
k = namify(kv.split("(").first)
t = kv.split("(").last.split(")").first.split(", ")
end
# transform into localize and extract variables
# "Hello \(count) \(world)" -> "Hello %d %@", [count, world]
s, v = variabilize(s, t)
if t.count == 0 && v.count > 0
puts "!!! ERROR: missing arguments types"
exit(0)
end
# build and write localizable.strings entry
# "HelloWorld" = "Hello %d %@";
l = "\"#{k}\" = \"#{s}\";"
File.open(filename, 'a') { |file| file.write(l + "\n") }
# add arguments to enum call
# tr(.HelloWorld(count, world))
if v.length > 0
vs = v.join(", ")
tr = "#{k}(#{vs})"
else
tr = k
end
else
puts "!!! ERROR: invalid syntax"
exit(0)
end
# re-adding annotated syntax at the end of tr syntax
# let a = tr(.Hello(world)) // translated from "Hello, \(world)", Hello(%@)
comment = localizable.split("// !tr").first.strip
comment += ", #{k}" if custom_key
comment += "(#{t.join(", ")})" if v.count > 0
# return tr syntax
puts "tr(.#{tr}) // translated from #{comment}"
#!/bin/bash
# Run this inside the project to replace annotated string calls with swiftgen calls in all .swift files.
# Do not forget to make a backup before.
# Usage: ./replaceAnnotatedLocalizeString.sh Localizable.strings
#
# Annotation look like this:
#
# Option 1
# let s = "Next Step" // !tr
# will be converted to:
# let s = tr(.NextStep) // translated from "Next Step"
# will add following string to localized.strings
# "NextStep" = "Next Step";
#
# Option 2
# let s = "Next Step" // !tr: MyKey
# will be converted to:
# let s = tr(.MyKey) // translated from "Next Step"
# will add following string to localized.strings
# "MyKey" = "Next Step";
#
# Option 3 (mandatory if arguments)
# let s = "Hello \(name), can you count to \(number)" // !tr: Greeting(%@, %d)
# will be converted to:
# let s = tr(.Greeting(name, number)) // translated from "Hello \(name), can you count to \(number)"
# will add following string to localized.strings
# "Greeting" = "Hello %@, can you count to %d";
#
# Limitations:
# - many and a lot unknown, undocumented
# - complex inner string variable expression: "Hello \(this + (that - 3))"
# - multi line string
# - more than string line of code:
# - replace: function(string: "My String \(blah)", options: []) // tr: String(%@)
# - by:
# - let aString = "My String \(blah)" // tr
# - function(string: aString, options: [])
#
# Recommendations:
# - add all annotations to your code, then commit, then run the script and diff
# - try to simplify your string assignement
#
# Please read your commits before commiting and then complaining.. :P
# check arguments
if [ "$#" -ne 1 ]; then
echo "Usage $0 path_to_localized_strings"
exit 1
fi
# get localizable.strings path
localizable_path=$1
# find all swift files
find . -type f | grep ".swift" > swiftindex.temp
while IFS= read -r filename
do
# extract all annotated lines
grep -o "\"[^\"]*\" // !tr: .*$" "$filename" > strings.temp
grep -o "\"[^\"]*\" // !tr$" "$filename" >> strings.temp
# process each lines
while IFS= read -r localizable
do
# call replacement script
replacement=$(ruby scripts/processAnnotatedString.rb "$localizable" "$localizable_path")
echo "$replacement"
# escaping replacement
localizable_escaped=$(echo "$localizable" | sed -e 's/[]\/$*.^|[]/\\&/g')
replacement_escaped=$(echo "$replacement" | sed -e 's/[]\/$*.^|[]/\\&/g')
# replacing in files (we could use line number...)
sed -i .bak "s/$localizable_escaped/$replacement_escaped/g" $filename
rm "$filename.bak"
done < strings.temp
rm strings.temp
done < swiftindex.temp
rm swiftindex.temp
#!/usr/bin/env ruby
# testing processAnnotatedString
def expect(a, b, test)
if a == b
puts "OK: #{test} >>>> #{a}"
else
puts "KO: #{test}:\ngot:\t\t#{b}\nexpecting:\t#{a}"
end
end
def test(string, expectedTR, expectedLocalizable)
tmp = "localizable.tmp"
r = IO.popen(['ruby', "scripts/processAnnotatedString.rb", string, tmp]).read.strip
expect(expectedTR, r, string)
r = File.exist?(tmp) && File.read(tmp).strip() || ""
expect(expectedLocalizable, r, string)
File.exist?(tmp) && File.delete(tmp)
end
test('"Hello World" // !tr', 'tr(.HelloWorld) // translated from "Hello World"', '"HelloWorld" = "Hello World";')
test('"12 monkeys" // !tr', 'tr(._12Monkeys) // translated from "12 monkeys"', '"_12Monkeys" = "12 monkeys";')
test('"It\'s complex" // !tr', 'tr(.ItSComplex) // translated from "It\'s complex"', '"ItSComplex" = "It\'s complex";')
test('"Hello \(world)" // !tr: Hello(%@)', 'tr(.Hello(world)) // translated from "Hello \(world)", Hello(%@)', '"Hello" = "Hello %@";')
test('"Hello again" // !tr: Hello', 'tr(.Hello) // translated from "Hello again", Hello', '"Hello" = "Hello again";')
test('"Hello \(world) from \(count) humans" // !tr: Hello(%@, %d)', 'tr(.Hello(world, count)) // translated from "Hello \(world) from \(count) humans", Hello(%@, %d)', '"Hello" = "Hello %@ from %d humans";')
test('"Hello \(world) from \(count) humans" // !tr: Hello(%@, %d)', 'tr(.Hello(world, count)) // translated from "Hello \(world) from \(count) humans", Hello(%@, %d)', '"Hello" = "Hello %@ from %d humans";')
test('"Hello \(world) from \(count) humans" // !tr', '!!! ERROR: missing arguments types', '')
@huguesbr
Copy link
Author

Automation of "annotated" swift localizable strings conversion, to be use with https://github.com/AliSoftware/SwiftGen
Heavily inspired of https://gist.github.com/Lutzifer/3e7d967f73e38b57d4355f23274f303d
Run this inside the project to replace annotated string (see exemple below) with swiftgen enum in all .swift files, and populate the Localizable.strings's file.
!!! Do not forget to make a backup before !!!
Usage: ./replaceAnnotatedLocalizeString.sh Localizable.strings

Supported syntax:
let s = "Next Step" // !tr
let s = "Next Step" // !tr: MyKey
let s = "Hello \(name), can you count to \(number)" // !tr: Greeting(%@, %d)

See more information in replaceAnnotatedLocalizeString.sh's header

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment