Skip to content

Commit

Permalink
Merge pull request #7 from lvsti/pseudo-localization
Browse files Browse the repository at this point in the history
Pseudo localization
  • Loading branch information
lvsti authored Oct 18, 2018
2 parents c857bc6 + 7aa7618 commit 0d3483c
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 29 deletions.
8 changes: 8 additions & 0 deletions Ilion.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
E85387D91EA6B080003184B4 /* BrowserWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = E85387D71EA6B080003184B4 /* BrowserWindow.xib */; };
E85387DA1EA6B080003184B4 /* BrowserWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85387D81EA6B080003184B4 /* BrowserWindowController.swift */; };
E85387DC1EA6B1AF003184B4 /* Ilion.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85387DB1EA6B1AF003184B4 /* Ilion.swift */; };
E85E7BA0216E9CF400858F2B /* Translation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85E7B9F216E9CF400858F2B /* Translation.swift */; };
E85E7BA2216E9F0900858F2B /* String+PseudoLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E85E7BA1216E9F0900858F2B /* String+PseudoLanguage.swift */; };
E8802A251EAE73B5005DFAF0 /* Dictionary+Fmap.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8802A241EAE73B5005DFAF0 /* Dictionary+Fmap.swift */; };
E8802A271EAE8472005DFAF0 /* String+RelativePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8802A261EAE8472005DFAF0 /* String+RelativePath.swift */; };
E8AA66DE1EB638A100903163 /* FormatDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8AA66DD1EB638A100903163 /* FormatDescriptor.swift */; };
Expand Down Expand Up @@ -51,6 +53,8 @@
E85387D71EA6B080003184B4 /* BrowserWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BrowserWindow.xib; sourceTree = "<group>"; };
E85387D81EA6B080003184B4 /* BrowserWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserWindowController.swift; sourceTree = "<group>"; };
E85387DB1EA6B1AF003184B4 /* Ilion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ilion.swift; sourceTree = "<group>"; };
E85E7B9F216E9CF400858F2B /* Translation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translation.swift; sourceTree = "<group>"; };
E85E7BA1216E9F0900858F2B /* String+PseudoLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+PseudoLanguage.swift"; sourceTree = "<group>"; };
E8802A241EAE73B5005DFAF0 /* Dictionary+Fmap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Fmap.swift"; sourceTree = "<group>"; };
E8802A261EAE8472005DFAF0 /* String+RelativePath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+RelativePath.swift"; sourceTree = "<group>"; };
E8AA66DD1EB638A100903163 /* FormatDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormatDescriptor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -111,9 +115,11 @@
E8B46F0C1EF5A46500A9C6F5 /* NSAttributedString+TokenMarkup.swift */,
E8136A691EA7E155003D9877 /* NSBundle+Ilion.m */,
E81AE5C61EC4FED30034EF07 /* String+Length.swift */,
E85E7BA1216E9F0900858F2B /* String+PseudoLanguage.swift */,
E8802A261EAE8472005DFAF0 /* String+RelativePath.swift */,
E8DCAA231ECF8E8E007F8915 /* StringsFileParser.swift */,
E85387D11EA6AD2E003184B4 /* StringsManager.swift */,
E85E7B9F216E9CF400858F2B /* Translation.swift */,
E8B46F081EF5986D00A9C6F5 /* TokenCell.swift */,
E80F44AB216A96DF00EF273A /* ToolsPanel.xib */,
E80F44A9216A96A700EF273A /* ToolsPanelController.swift */,
Expand Down Expand Up @@ -209,12 +215,14 @@
E8DCAA241ECF8E8E007F8915 /* StringsFileParser.swift in Sources */,
E8AA66DE1EB638A100903163 /* FormatDescriptor.swift in Sources */,
E85387DC1EA6B1AF003184B4 /* Ilion.swift in Sources */,
E85E7BA2216E9F0900858F2B /* String+PseudoLanguage.swift in Sources */,
E84EA94F1EB3C858001C6856 /* IlionLauncher.m in Sources */,
E8B027D01EC1198B0006CA51 /* LocalizedFormat.swift in Sources */,
E8802A271EAE8472005DFAF0 /* String+RelativePath.swift in Sources */,
E81AE5CB1EC74F2D0034EF07 /* EditPanelViewModel.swift in Sources */,
E85387D61EA6AF9B003184B4 /* EditPanelController.swift in Sources */,
E8802A251EAE73B5005DFAF0 /* Dictionary+Fmap.swift in Sources */,
E85E7BA0216E9CF400858F2B /* Translation.swift in Sources */,
E85387DA1EA6B080003184B4 /* BrowserWindowController.swift in Sources */,
E80F44AA216A96A700EF273A /* ToolsPanelController.swift in Sources */,
E8B46F091EF5986D00A9C6F5 /* TokenCell.swift in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions Ilion/Ilion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ extension Ilion: BrowserWindowControllerDelegate {

toolsPanelController = ToolsPanelController()
toolsPanelController?.shouldInsertStartEndMarkers = StringsManager.defaultManager.insertsStartEndMarkers
toolsPanelController?.shouldTransformCharacters = StringsManager.defaultManager.transformsCharacters
toolsPanelController?.delegate = self

browserWindowController?.window?.beginSheet(toolsPanelController!.window!)
Expand Down Expand Up @@ -154,11 +155,13 @@ extension Ilion: ToolsPanelControllerDelegate {

func toolsPanelControllerDidClose(_ sender: ToolsPanelController) {
let markersFlag = toolsPanelController!.shouldInsertStartEndMarkers
let transformFlag = toolsPanelController!.shouldTransformCharacters

browserWindowController?.window?.endSheet(sender.window!)
toolsPanelController = nil

StringsManager.defaultManager.insertsStartEndMarkers = markersFlag
StringsManager.defaultManager.transformsCharacters = transformFlag
}

}
67 changes: 65 additions & 2 deletions Ilion/LocalizedFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,73 @@ struct LocalizedFormat {
"format": VariableSpec(valueType: valueType, ruleSpecs: formats)
]
}

private init(baseFormat: String, variableSpecs: [String: VariableSpec]) {
self.baseFormat = baseFormat
self.variableSpecs = variableSpecs
}

func appending(_ string: String) -> LocalizedFormat {
return LocalizedFormat(baseFormat: baseFormat.appending(string), variableSpecs: variableSpecs)
}

func prepending(_ string: String) -> LocalizedFormat {
return LocalizedFormat(baseFormat: string.appending(baseFormat), variableSpecs: variableSpecs)
}

func applyingTransform(_ transform: ([String]) -> [String]) -> LocalizedFormat {
var config = toStringsDictEntry()
let ruleNames = Set(PluralRule.allCases.map({ $0.rawValue }))

func slice(_ str: String, by indices: IndexSet) -> [String] {
return indices.rangeView.map { range in
let sliceStart = str.index(str.startIndex, offsetBy: range.startIndex)
let sliceEnd = str.index(str.startIndex, offsetBy: range.endIndex)
return String(str[sliceStart..<sliceEnd])
}
}

var updatedConfig = config

for (varName, varConfig) in config where varName != "NSStringLocalizedFormatKey" {
let originalVarConfig = varConfig as! [String: String]
var updatedVarConfig = originalVarConfig

for (ruleName, format) in originalVarConfig where ruleNames.contains(ruleName) {
let variableIndices = FormatDescriptor.variableRanges(in: format)
.map { IndexSet(integersIn: Range($0)!) }
.reduce(IndexSet()) { acc, next in
acc.union(next)
}

let literalIndices = IndexSet(integersIn: 0..<format.count).subtracting(variableIndices)
let literalSlices = slice(format, by: literalIndices)
let transformedLiteralSlices = transform(literalSlices)

let variableSlices = slice(format, by: variableIndices)
let startsWithVariable = variableIndices.contains(0)

let firstSource = startsWithVariable ? variableSlices : transformedLiteralSlices
let secondSource = startsWithVariable ? transformedLiteralSlices : variableSlices

let transformedFormat = zip(firstSource, secondSource + [""]).reduce(String()) { acc, next in
acc.appending(next.0).appending(next.1)
}

updatedVarConfig[ruleName] = transformedFormat
}

updatedConfig[varName] = originalVarConfig.merging(updatedVarConfig, uniquingKeysWith: { $1 })
}

config.merge(updatedConfig, uniquingKeysWith: { $1 })

return try! LocalizedFormat(config: config)
}

func toStringsDictEntry(insertingStartEndMarkers insertMarkers: Bool = false) -> [String: Any] {
func toStringsDictEntry() -> [String: Any] {
var config: [String: Any] = [
"NSStringLocalizedFormatKey": insertMarkers ?"[\(baseFormat)]" : baseFormat
"NSStringLocalizedFormatKey": baseFormat
]

for (varName, varSpec) in variableSpecs {
Expand Down
45 changes: 45 additions & 0 deletions Ilion/String+PseudoLanguage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// String+PseudoLanguage.swift
// Ilion
//
// Created by Tamas Lustyik on 2018. 10. 10..
// Copyright © 2018. Tamas Lustyik. All rights reserved.
//

import Foundation

extension String {
func applyingPseudoLanguageTransformation() -> String {
let pseudoLatin = "αɓ¢ԃεƒϑჩℹ︎ʝҝƖოηфϸƪʀς†цγш×уƶΛßϚЀƑ₲ҤǀͿƘ£ℳИØ₱Ɋ®ʃƬЦƲШχ¥Ɀ"
let combiningDiacriticalRange: Range<UInt32> = 0x300..<0x370
let isBasicLatinLower: (UInt32) -> Bool = { (0x61...0x7A).contains($0) }
let isBasicLatinUpper: (UInt32) -> Bool = { (0x41...0x5A).contains($0) }
let isBasicLatin: (UInt32) -> Bool = { isBasicLatinLower($0) || isBasicLatinUpper($0) }

var scalars = decomposedStringWithCanonicalMapping
.flatMap { Array($0.unicodeScalars) }
.map { $0.value }

var insertions: [(Int, UInt32)] = []
for (idx, scalar) in scalars.enumerated() where isBasicLatin(scalar) {
// character replacement
if Bool.random() {
let offset = isBasicLatinLower(scalar) ? Int(scalar - 0x61) : Int(scalar - 0x41)
let ch = pseudoLatin[pseudoLatin.index(pseudoLatin.startIndex, offsetBy: offset)]
scalars[idx] = ch.unicodeScalars.first!.value
}

// diacritic decoration
for _ in 0..<(1...4).randomElement()! {
let diacritic = combiningDiacriticalRange.randomElement()!
insertions.append((idx + 1, diacritic))
}
}

for insertion in insertions.reversed() {
scalars.insert(insertion.1, at: insertion.0)
}

return String(String.UnicodeScalarView(scalars.map { Unicode.Scalar($0)! }))
}
}
37 changes: 19 additions & 18 deletions Ilion/StringsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
import Foundation


enum Translation {
case `static`(String)
case `dynamic`(LocalizedFormat)
}

struct StringsEntry {
var locKey: LocKey
var comment: String?
Expand All @@ -38,6 +33,7 @@ typealias StringsDB = [BundleURI: [ResourceURI: [LocKey: StringsEntry]]]

private let storedOverridesKey = "Ilion.TranslationOverrides"
private let markersKey = "Ilion.InsertsStartEndMarkers"
private let transformKey = "Ilion.TransformsCharacters"

private let userDefaults: UserDefaults
private let stringsFileParser: StringsFileParser
Expand All @@ -51,7 +47,13 @@ typealias StringsDB = [BundleURI: [ResourceURI: [LocKey: StringsEntry]]]
userDefaults.setValue(insertsStartEndMarkers, forKey: markersKey)
}
}


var transformsCharacters: Bool = false {
didSet {
userDefaults.setValue(transformsCharacters, forKey: transformKey)
}
}

@objc static let defaultManager = StringsManager(userDefaults: .standard, stringsFileParser: StringsFileParser())

private init(userDefaults: UserDefaults, stringsFileParser: StringsFileParser) {
Expand All @@ -62,6 +64,7 @@ typealias StringsDB = [BundleURI: [ResourceURI: [LocKey: StringsEntry]]]
db = [:]
overriddenKeyPaths = []
insertsStartEndMarkers = userDefaults.value(forKey: markersKey) as? Bool ?? false
transformsCharacters = userDefaults.value(forKey: transformKey) as? Bool ?? false

super.init()

Expand Down Expand Up @@ -140,20 +143,18 @@ typealias StringsDB = [BundleURI: [ResourceURI: [LocKey: StringsEntry]]]
let entry = db[bundleURI]?[stringsDictResourceURI]?[key] ?? db[bundleURI]?[stringsResourceURI]?[key]
else {
let baseCopy = (value?.isEmpty ?? true) ? key : value!
return insertsStartEndMarkers ? "[\(baseCopy)]" : baseCopy
return [Translation.static(baseCopy)]
.map { transformsCharacters ? $0.applyingPseudoLocalization() : $0 }
.map { insertsStartEndMarkers ? $0.addingStartEndMarkers() : $0 }
.first!
.toString()
}

let translation = entry.override ?? entry.translation

switch translation {
case .static(let text): return insertsStartEndMarkers ? "[\(text)]" : text
case .dynamic(let format):
let config = format.toStringsDictEntry(insertingStartEndMarkers: insertsStartEndMarkers)
let nsFormat = format.baseFormat as NSString
let locFormat = nsFormat.perform(NSSelectorFromString("_copyFormatStringWithConfiguration:"), with: config)
.takeUnretainedValue()
return locFormat as! String
}
return [entry.override ?? entry.translation]
.map { transformsCharacters ? $0.applyingPseudoLocalization() : $0 }
.map { insertsStartEndMarkers ? $0.addingStartEndMarkers() : $0 }
.first!
.toString()
}

// MARK: - internal Swift API
Expand Down
21 changes: 17 additions & 4 deletions Ilion/ToolsPanel.xib
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<customObject id="-2" userLabel="File's Owner" customClass="ToolsPanelController" customModule="Ilion" customModuleProvider="target">
<connections>
<outlet property="markersCheckbox" destination="VXA-57-yvZ" id="8Xl-q5-j4f"/>
<outlet property="transformCheckbox" destination="59J-LL-Tct" id="uMc-TC-obV"/>
<outlet property="window" destination="QvC-M9-y7g" id="Ewr-HP-uli"/>
</connections>
</customObject>
Expand All @@ -17,14 +18,14 @@
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="NSPanel">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="389" height="98"/>
<rect key="contentRect" x="196" y="240" width="389" height="115"/>
<rect key="screenRect" x="0.0" y="0.0" width="1280" height="777"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="389" height="98"/>
<rect key="frame" x="0.0" y="0.0" width="389" height="115"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VXA-57-yvZ">
<rect key="frame" x="18" y="62" width="169" height="18"/>
<rect key="frame" x="18" y="79" width="169" height="18"/>
<buttonCell key="cell" type="check" title="Insert start/end markers" bezelStyle="regularSquare" imagePosition="left" inset="2" id="zoO-2q-V83">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
Expand All @@ -46,15 +47,27 @@ DQ
<action selector="doneClicked:" target="-2" id="eLu-Ja-eTl"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="59J-LL-Tct">
<rect key="frame" x="18" y="57" width="222" height="18"/>
<buttonCell key="cell" type="check" title="Fuzzy transform Latin characters" bezelStyle="regularSquare" imagePosition="left" inset="2" id="X66-EC-Nnl">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="checkboxToggled:" target="-2" id="SHd-7x-XlM"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="59J-LL-Tct" firstAttribute="top" secondItem="VXA-57-yvZ" secondAttribute="bottom" constant="8" id="Aku-Mn-Lou"/>
<constraint firstAttribute="bottom" secondItem="rpe-tP-MWG" secondAttribute="bottom" constant="20" id="DUF-Fn-F55"/>
<constraint firstItem="59J-LL-Tct" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="bed-zX-iBd"/>
<constraint firstItem="VXA-57-yvZ" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" id="bjX-ZG-m5M"/>
<constraint firstAttribute="trailing" secondItem="rpe-tP-MWG" secondAttribute="trailing" constant="20" id="j3q-ni-fpr"/>
<constraint firstItem="VXA-57-yvZ" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" id="oGp-39-McN"/>
</constraints>
</view>
<point key="canvasLocation" x="93.5" y="61"/>
<point key="canvasLocation" x="93.5" y="69.5"/>
</window>
</objects>
</document>
12 changes: 11 additions & 1 deletion Ilion/ToolsPanelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ protocol ToolsPanelControllerDelegate: class {

final class ToolsPanelController: NSWindowController {
@IBOutlet private weak var markersCheckbox: NSButton!
@IBOutlet private weak var transformCheckbox: NSButton!

weak var delegate: ToolsPanelControllerDelegate?

Expand All @@ -23,18 +24,27 @@ final class ToolsPanelController: NSWindowController {
markersCheckbox?.state = shouldInsertStartEndMarkers ? .on : .off
}
}


var shouldTransformCharacters: Bool = false {
didSet {
guard isWindowLoaded else { return }
transformCheckbox?.state = shouldTransformCharacters ? .on : .off
}
}

override var windowNibName: NSNib.Name? {
return "ToolsPanel"
}

override func windowDidLoad() {
super.windowDidLoad()
markersCheckbox.state = shouldInsertStartEndMarkers ? .on : .off
transformCheckbox.state = shouldTransformCharacters ? .on : .off
}

@IBAction private func checkboxToggled(_ sender: Any) {
shouldInsertStartEndMarkers = markersCheckbox.state == .on
shouldTransformCharacters = transformCheckbox.state == .on
}

@IBAction private func doneClicked(_ sender: Any) {
Expand Down
Loading

0 comments on commit 0d3483c

Please sign in to comment.