How to apply translations to Localizable.strings

Issue #492

Suppose we have a base Localizable.strings

1
2
"open" = "Open";
"closed" = "Closed";

After sending that file for translations, we get translated versions.

1
2
"open" = "Åpen";
"closed" = "Stengt";

Searching and copy pasting these to our Localizable.strings is tedious and time consuming. We can write a script to apply that.

Remember that we need to be aware of smart and dump quotes

1
2
.replace(/\"/g, '')
.replace(/\"/g, '')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const fs = require('fs')

const originalFile = 'MyApp/Resources/nb.lproj/Localizable.strings'
const translationString = `
"open" = "Åpen";
"closed" = "Stengt";
`

class Translation {
constructor(key, value) {
this.key = key
this.value = value
}
}

Translation.make = function make(line) {
if (!line.endsWith(';')) {
return new Translation('', '')
}

const parts = line
.replace(';')
.split(" = ")

const key = parts[0]
.replace(/\"/g, '')
.replace(/\”/g, '')
const value = parts[1]
.replace(/\"/g, '')
.replace(/\”/g, '')
.replace('undefined', '')
return new Translation(key, value)
}

function main() {
const translations = translationString
.split(/\r?\n/)
.map((line) => { return Translation.make(line) })

apply(translations, originalFile)
}

function apply(translations, originalFile) {
try {
const originalData = fs.readFileSync(originalFile, 'utf8')
const originalLines = originalData.split(/\r?\n/)
const parsedLine = originalLines.map((originalLine) => {
const originalTranslation = Translation.make(originalLine)
const find = translations.find((translation) => { return translation.key === originalTranslation.key })
if (originalLine !== "" && find !== undefined && find.key !== "") {
return `"${find.key}" = "${find.value}";`
} else {
return originalLine
}
})

const parsedData = parsedLine.join('\n')
fs.writeFileSync(originalFile, parsedData, { overwrite: true })
} catch (err) {
console.error(err)
}
}

main()

How to remove Cartography in iOS

Issue #252

Read more https://medium.com/flawless-app-stories/how-to-make-auto-layout-more-convenient-in-ios-df3b42fed37f

Description

This is a script to remove Cartography, and use plain NSLayoutAnchor syntax.
Use Constraint.on() from Sugar.
It will change all .swift files recursively under provided folder.

1
2
3
4
Constraint.on(
logoImageView.widthAnchor.constraint(equalToConstant: 74),
view1.leftAnchor.constraint(equalTo: view2.leftAnchor, constant: 20),
)

Features

  • Parse recursively
  • Parse a single file
  • Parse each constrain block separately
  • Handle ==, >=, <=
  • Handle center, edges
  • Infer indentation
  • Handle relation with other item
  • Handle relation with constant
  • Handle multiplier
  • Auto import Sugar
  • Infer superView

Prepare

Install tool if needed

1
brew install yarn

How to use

1
2
yarn install
yarn start /Users/khoa/path/to/project

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300

const Glob = require('glob')
const Fs = require('fs')

function main() {
const projectPath = getProjectPath()
const files = getAllFiles(projectPath)

files.forEach((file) => {
handleFile(file)
})
}

/// The 1st argument is node, 2nd is index.js, 3rd is path
function getProjectPath() {
return process.argv[2]
}

/// Only select `swift` file
function getAllFiles(projectPath) {
if (projectPath.endsWith('.swift')) {
return [projectPath]
} else {
const files = Glob.sync(`${projectPath}/**/*.swift`)
return files
}
}

/// Read file, replace by matches, and write again
function handleFile(file) {
console.log(file)

Fs.readFile(file, 'utf8', (error, data) => {
// non greedy `*?`
const pattern = '(.)*constrain\\(.+\\) {\\s(\\s|.)*?\\n(\\s)*}'
const regex = new RegExp(pattern)

let matches = data.match(regex)

// RegEx only return the 1st match per execution, let's do a recursion
while (matches != null) {
const match = matches[0]

const indentationLength = findIndentationLength(match)
const rawTransforms = handleMatch(match)
const transforms = handleSuperview(rawTransforms, match)
const statements = handleTransforms(transforms, indentationLength)
const string = handleStatements(statements, indentationLength)
data = data.replace(match, string)

// Examine again
matches = data.match(regex)
}

// Handle import
data = handleImport(data)


Fs.writeFile(file, data, 'utf8')
})
}

/// Replace or remove import
function handleImport(data) {
if (data.includes('import Sugar')) {
return data.replace('import Cartography', '')
} else {
return data.replace('import Cartography', 'import Sugar')
}
}

/// Format transform to have `,` and linebreaks
function handleTransforms(transforms, indentationLength) {
let string = ''

// Expect the first is always line break
if (transforms[0] != '\n') {
transforms.unshift('\n')
}

const indentation = makeIndentation(indentationLength + 2)

transforms.forEach((line, index) => {
if (line.length > 1) {
string = string.concat(indentation + line)
if (index < transforms.length - 1) {
string = string.concat(',\n')
}
} else {
string = string.concat('\n')
}
})

return string
}

/// Replace superView
function handleSuperview(transforms, match) {
const superView = findSuperview(match)
if (superView == null) {
return transforms
}

const superViewPattern = ' superView.'
transforms = transforms.map((transform) => {
if (transform.includes(superViewPattern)) {
return transform.replace(superViewPattern, ` ${superView}.`)
} else {
return transform
}
})

return transforms
}

/// Embed the statements inside `Constraint.on`
function handleStatements(statements, indentationLength) {
const indentation = makeIndentation(indentationLength)
return `${indentation}Constraint.on(${statements}\n${indentation})`
}

/// Turn every line into flatten transformed line
function handleMatch(match) {
let lines = match.split('\n')
lines = lines.map((line) => {
return handleLine(line.trim())
}).filter((transforms) => {
return transforms != null
})

let flatten = [].concat.apply([], lines)
return flatten
}

/// Check to handle lines, turn them into `LayoutAnchor` statements
function handleLine(line) {
const itemPattern = '\\w*\\.\\w* (=|<|>)= \\w*\\.*\\..*'
const sizePattern = '\w*\.\w* (=|<|>)= (\s|.)*'

if (line.includes('edges == ')) {
return handleEdges(line)
} else if (line.includes('center == ')) {
return handleCenter(line)
} else if (hasPattern(line, itemPattern) && !hasSizeKeywords(line)) {
return handleItem(line)
} else if (hasPattern(line, sizePattern)) {
return handleSize(line)
} else if (line.includes('>=') || line.includes('>=')) {
return line
} else if (line.includes('.')) {
// return the line itself to let the human fix
return line
} else if (line.length == 0) {
return ['\n']
} else {
return null
}
}

/// For ex: listView.bottom == listView.superview!.bottom - 43
/// listView.bottom == listView.superview!.bottom - Metrics.BackButtonWidth
function handleItem(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]

let rightParts = parts[1].trim()
let right = rightParts.split(' ')[0]
let number = rightParts.replace(right, '').replace('- ', '-').trim()
if (number.startsWith('+ ')) {
number = number.slice(2)
}

let equal = getEqual(line)

if (number == null || number.length == 0 ) {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor)`
]
} else {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor, constant: ${number})`
]
}
}

/// For ex: segmentedControl.height == 24
/// backButton.width == Metrics.BackButtonWidth
function handleSize(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]
const right = parts[1]

let equal = getEqual(line)

return [
`${left}Anchor.constraint(${equal}Constant: ${right})`
]
}

/// For ex: mapView.edges == listView.edges
function handleEdges(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])

return [
`${left}.topAnchor.constraint(equalTo: ${right}.topAnchor)`,
`${left}.bottomAnchor.constraint(equalTo: ${right}.bottomAnchor)`,
`${left}.leftAnchor.constraint(equalTo: ${right}.leftAnchor)`,
`${left}.rightAnchor.constraint(equalTo: ${right}.rightAnchor)`
]
}

/// For ex: mapView.center == listView.center
function handleCenter(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])

return [
`${left}.centerXAnchor.constraint(equalTo: ${right}.centerXAnchor)`,
`${left}.centerYAnchor.constraint(equalTo: ${right}.centerYAnchor)`
]
}

function hasPattern(string, pattern) {
const regex = new RegExp(pattern)
let matches = string.match(regex)

if (matches == null) {
return false
}

matches = matches.filter((match) => {
return match !== undefined && match.length > 1
})

return matches.length > 0
}

function removeLastPart(string) {
const parts = string.split('.')
parts.pop()

return parts.join('.')
}

function getEqual(line) {
if (line.includes('==')) {
return 'equalTo'
} else if (line.includes('<=')) {
return 'lessThanOrEqualTo'
} else {
return 'greaterThanOrEqualTo'
}
}

function getEqualSign(line) {
if (line.includes('==')) {
return '=='
} else if (line.includes('>=')) {
return '>='
} else {
return '<='
}
}

function findIndentationLength(match) {
return match.split('constrain')[0].length + 1
}

function makeIndentation(length) {
return Array(length).join(' ')
}

// For ex: constrain(tableView, headerView, view) { tableView, headerView, superView in
function findSuperview(match) {
const line = match.split('\n')[0]
if (!line.includes(' superView in')) {
return null
}

const pattern = ', \\w*\\)'
const regex = RegExp(pattern)
const string = line.match(regex)[0] // `, view)

return string.replace(', ', '').replace(')', '')
}

// Check special size keywords
function hasSizeKeywords(line) {
const keywords = ['Metrics', 'Dimensions', 'UIScreen']
return keywords.filter((keyword) => {
return line.includes(keyword)
}).length != 0
}

main()