Compare commits
11 Commits
0721b7f9d4
...
v0.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 01bf493c77 | |||
| b6e9ad6160 | |||
| c56c0ae198 | |||
| 6b94030b83 | |||
| 2bbd722ecd | |||
| 73f16fcfef | |||
| 9df6531023 | |||
| 1db2ca3395 | |||
| 419b2f5a79 | |||
| 44607ba9e2 | |||
| f4d2f4e6eb |
61
README.md
61
README.md
@@ -12,6 +12,7 @@
|
||||
### Features
|
||||
|
||||
- Notification relay
|
||||
- Notificstion transliteration
|
||||
- Music control
|
||||
- Get info from watch (HRM, Battery level, Firmware version)
|
||||
- Set current time
|
||||
@@ -36,6 +37,42 @@ The various request types and their data requirements can be seen in `internal/t
|
||||
|
||||
---
|
||||
|
||||
### Transliteration
|
||||
|
||||
Since the PineTime does not have enough space to store all unicode glyphs, it only stores the ASCII space and Cyrillic. Therefore, this daemon can transliterate unsupported characters into supported ones. Since some languages have different transliterations, the transliterators to be used must be specified in the config. Here are the available transliterators:
|
||||
|
||||
- eASCII
|
||||
- Scandinavian
|
||||
- German
|
||||
- Hebrew
|
||||
- Greek
|
||||
- Russian
|
||||
- Ukranian
|
||||
- Arabic
|
||||
- Farsi
|
||||
- Polish
|
||||
- Lithuanian
|
||||
- Estonian
|
||||
- Icelandic
|
||||
- Czeck
|
||||
- French
|
||||
- Armenian
|
||||
- Korean
|
||||
- Chinese
|
||||
- Emoji
|
||||
|
||||
Place the desired map names in an array as `notifs.translit.use`. They will be evaluated in order. You can also put custom transliterations in `notifs.translit.custom`. These take priority over any other maps. The `notifs.translit` config section should look like this:
|
||||
|
||||
```toml
|
||||
[notifs.translit]
|
||||
use = ["eASCII", "Russian", "Emoji"]
|
||||
custom = [
|
||||
"test", "replaced"
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `itctl`
|
||||
|
||||
This daemon comes with a binary called `itctl` which uses the socket to control the daemon from the command line. As such, it can be scripted using bash.
|
||||
@@ -61,6 +98,30 @@ Flags:
|
||||
Use "itctl [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `itgui`
|
||||
|
||||
In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [fyne library](https://fyne.io/) for Go. It can be compiled by running:
|
||||
|
||||
```shell
|
||||
go build ./cmd/itgui
|
||||
```
|
||||
|
||||
#### Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
#### Interactive mode
|
||||
|
||||
Running `itctl` by itself will open interactive mode. It's essentially a shell where you can enter commands. For example:
|
||||
|
||||
@@ -95,14 +95,14 @@ var upgradeCmd = &cobra.Command{
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error decoding response data")
|
||||
}
|
||||
// If transfer finished, break
|
||||
if event.Received == event.Total {
|
||||
break
|
||||
}
|
||||
// Set total bytes in progress bar
|
||||
bar.SetTotal(event.Total)
|
||||
// Set amount of bytes received in progress bar
|
||||
bar.SetCurrent(event.Received)
|
||||
// If transfer finished, break
|
||||
if event.Received == event.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Finish progress bar
|
||||
bar.Finish()
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"os"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func guiErr(err error, msg string, parent fyne.Window) {
|
||||
func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
|
||||
// Create new label containing message
|
||||
msgLbl := widget.NewLabel(msg)
|
||||
// Text formatting settings
|
||||
@@ -33,6 +34,20 @@ func guiErr(err error, msg string, parent fyne.Window) {
|
||||
widget.NewAccordionItem("More Details", errLbl),
|
||||
))
|
||||
}
|
||||
// Show error dialog
|
||||
dialog.NewCustom("Error", "Ok", content, parent).Show()
|
||||
if fatal {
|
||||
// Create new error dialog
|
||||
errDlg := dialog.NewCustom("Error", "Close", content, parent)
|
||||
// On close, exit with code 1
|
||||
errDlg.SetOnClosed(func() {
|
||||
os.Exit(1)
|
||||
})
|
||||
// Show dialog
|
||||
errDlg.Show()
|
||||
// Run app prematurely to stop further execution
|
||||
parent.ShowAndRun()
|
||||
} else {
|
||||
// Show error dialog
|
||||
dialog.NewCustom("Error", "Ok", content, parent).Show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func infoTab(parent fyne.Window) *fyne.Container {
|
||||
|
||||
fwVerString, err := get(types.ReqTypeFwVersion)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
guiErr(err, "Error getting firmware string", true, parent)
|
||||
}
|
||||
|
||||
fwVer := container.NewVBox(
|
||||
@@ -99,7 +99,7 @@ func watch(req int, onRecv func(data interface{}), parent fyne.Window) error {
|
||||
for scanner.Scan() {
|
||||
res, err := getResp(scanner.Bytes())
|
||||
if err != nil {
|
||||
guiErr(err, "Error getting response from connection", parent)
|
||||
guiErr(err, "Error getting response from connection", false, parent)
|
||||
continue
|
||||
}
|
||||
onRecv(res.Value)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
)
|
||||
@@ -13,6 +15,11 @@ func main() {
|
||||
// Create new window with title "itgui"
|
||||
window := a.NewWindow("itgui")
|
||||
|
||||
_, err := net.Dial("unix", SockPath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error dialing itd socket", true, window)
|
||||
}
|
||||
|
||||
// Create new app tabs container
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Info", infoTab(window)),
|
||||
|
||||
@@ -25,7 +25,7 @@ func notifyTab(parent fyne.Window) *fyne.Container {
|
||||
// Dial itd UNIX socket
|
||||
conn, err := net.Dial("unix", SockPath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error dialing socket", parent)
|
||||
guiErr(err, "Error dialing socket", false, parent)
|
||||
return
|
||||
}
|
||||
// Encode notify request on connection
|
||||
|
||||
@@ -29,7 +29,7 @@ func timeTab(parent fyne.Window) *fyne.Container {
|
||||
// Parse time as RFC1123 string
|
||||
parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text)
|
||||
if err != nil {
|
||||
guiErr(err, "Error parsing time string", parent)
|
||||
guiErr(err, "Error parsing time string", false, parent)
|
||||
return
|
||||
}
|
||||
// Set time to parsed time
|
||||
|
||||
@@ -92,7 +92,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
|
||||
// If archive path does not exist and both init packet and firmware paths
|
||||
// also do not exist, return error
|
||||
if archivePath == "" && (initPktPath == "" && fiwmarePath == "") {
|
||||
guiErr(nil, "Upgrade requires archive or files selected", parent)
|
||||
guiErr(nil, "Upgrade requires archive or files selected", false, parent)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
|
||||
// Dial itd UNIX socket
|
||||
conn, err := net.Dial("unix", SockPath)
|
||||
if err != nil {
|
||||
guiErr(err, "Error dialing socket", parent)
|
||||
guiErr(err, "Error dialing socket", false, parent)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
@@ -150,24 +150,20 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
|
||||
// Decode scanned line into response struct
|
||||
err = json.Unmarshal(scanner.Bytes(), &res)
|
||||
if err != nil {
|
||||
guiErr(err, "Error decoding response", parent)
|
||||
guiErr(err, "Error decoding response", false, parent)
|
||||
return
|
||||
}
|
||||
if res.Error {
|
||||
guiErr(err, "Error returned in response", parent)
|
||||
guiErr(err, "Error returned in response", false, parent)
|
||||
return
|
||||
}
|
||||
var event types.DFUProgress
|
||||
// Decode response data into progress struct
|
||||
err = mapstructure.Decode(res.Value, &event)
|
||||
if err != nil {
|
||||
guiErr(err, "Error decoding response value", parent)
|
||||
guiErr(err, "Error decoding response value", false, parent)
|
||||
return
|
||||
}
|
||||
// If transfer finished, break
|
||||
if event.Received == event.Total {
|
||||
break
|
||||
}
|
||||
// Set label text to received / total B
|
||||
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
|
||||
// Set progress bar values
|
||||
@@ -175,6 +171,10 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
|
||||
progressBar.Value = float64(event.Received)
|
||||
// Refresh progress bar
|
||||
progressBar.Refresh()
|
||||
// If transfer finished, break
|
||||
if event.Received == event.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
3
go.mod
3
go.mod
@@ -15,6 +15,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1
|
||||
github.com/mozillazg/go-pinyin v0.18.0
|
||||
github.com/rs/zerolog v1.23.0
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
@@ -22,5 +23,5 @@ require (
|
||||
github.com/spf13/viper v1.8.1
|
||||
go.arsenm.dev/infinitime v0.0.0-20210825051734-745b4bd37cf4
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/text v0.3.7
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@@ -242,6 +242,8 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mozillazg/go-pinyin v0.18.0 h1:hQompXO23/0ohH8YNjvfsAITnCQImCiR/Fny8EhIeW0=
|
||||
github.com/mozillazg/go-pinyin v0.18.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
|
||||
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d h1:EG/xyWjHT19rkUpwsWSkyiCCmyqNwFovr9m10rhyOxU=
|
||||
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
|
||||
3
itd.toml
3
itd.toml
@@ -15,6 +15,9 @@ cfg.version = 2
|
||||
notify = true
|
||||
setTime = true
|
||||
|
||||
[notifs.translit]
|
||||
use = ["eASCII", "Russian", "Emoji"]
|
||||
|
||||
[notifs.ignore]
|
||||
sender = []
|
||||
summary = ["InfiniTime"]
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/itd/translit"
|
||||
)
|
||||
|
||||
func initNotifRelay(dev *infinitime.Device) error {
|
||||
@@ -71,6 +72,12 @@ func initNotifRelay(dev *infinitime.Device) error {
|
||||
continue
|
||||
}
|
||||
|
||||
maps := viper.GetStringSlice("notifs.translit.use")
|
||||
translit.Transliterators["custom"] = translit.Map(viper.GetStringSlice("notifs.translit.custom"))
|
||||
sender = translit.Transliterate(sender, maps...)
|
||||
summary = translit.Transliterate(summary, maps...)
|
||||
body = translit.Transliterate(body, maps...)
|
||||
|
||||
var msg string
|
||||
// If summary does not exist, set message to body.
|
||||
// If it does, set message to summary, two newlines, and then body
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
"go.arsenm.dev/infinitime"
|
||||
"go.arsenm.dev/itd/internal/types"
|
||||
"go.arsenm.dev/itd/translit"
|
||||
)
|
||||
|
||||
func startSocket(dev *infinitime.Device) error {
|
||||
@@ -169,8 +170,12 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
|
||||
connErr(conn, err, "Error decoding request data")
|
||||
break
|
||||
}
|
||||
maps := viper.GetStringSlice("notifs.translit.use")
|
||||
translit.Transliterators["custom"] = translit.Map(viper.GetStringSlice("notifs.translit.custom"))
|
||||
title := translit.Transliterate(reqData.Title, maps...)
|
||||
body := translit.Transliterate(reqData.Body, maps...)
|
||||
// Send notification to watch
|
||||
err = dev.Notify(reqData.Title, reqData.Body)
|
||||
err = dev.Notify(title, body)
|
||||
if err != nil {
|
||||
connErr(conn, err, "Error sending notification")
|
||||
break
|
||||
|
||||
138
translit/armenian.go
Normal file
138
translit/armenian.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package translit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ArmenianTranslit struct{}
|
||||
|
||||
var armenianMap = []string{
|
||||
"աու", "au",
|
||||
"բու", "bu",
|
||||
"գու", "gu",
|
||||
"դու", "du",
|
||||
"եու", "eu",
|
||||
"զու", "zu",
|
||||
"էու", "eu",
|
||||
"ըու", "yu",
|
||||
"թու", "tu",
|
||||
"ժու", "ju",
|
||||
"իու", "iu",
|
||||
"լու", "lu",
|
||||
"խու", "xu",
|
||||
"ծու", "cu",
|
||||
"կու", "ku",
|
||||
"հու", "hu",
|
||||
"ձու", "dzu",
|
||||
"ղու", "xu",
|
||||
"ճու", "cu",
|
||||
"մու", "mu",
|
||||
"յու", "yu",
|
||||
"նու", "nu",
|
||||
"շու", "shu",
|
||||
"չու", "chu",
|
||||
"պու", "pu",
|
||||
"ջու", "ju",
|
||||
"ռու", "ru",
|
||||
"սու", "su",
|
||||
"վու", "vu",
|
||||
"տու", "tu",
|
||||
"րու", "ru",
|
||||
"ցու", "cu",
|
||||
"փու", "pu",
|
||||
"քու", "qu",
|
||||
"օու", "ou",
|
||||
"ևու", "eu",
|
||||
"ֆու", "fu",
|
||||
"ոու", "vou",
|
||||
"ու", "u",
|
||||
"բո", "bo",
|
||||
"գո", "go",
|
||||
"դո", "do",
|
||||
"զո", "zo",
|
||||
"թո", "to",
|
||||
"ժո", "jo",
|
||||
"լո", "lo",
|
||||
"խո", "xo",
|
||||
"ծո", "co",
|
||||
"կո", "ko",
|
||||
"հո", "ho",
|
||||
"ձո", "dzo",
|
||||
"ղո", "xo",
|
||||
"ճո", "co",
|
||||
"մո", "mo",
|
||||
"յո", "yo",
|
||||
"նո", "no",
|
||||
"շո", "so",
|
||||
"չո", "co",
|
||||
"պո", "po",
|
||||
"ջո", "jo",
|
||||
"ռո", "ro",
|
||||
"սո", "so",
|
||||
"վո", "vo",
|
||||
"տո", "to",
|
||||
"րո", "ro",
|
||||
"ցո", "co",
|
||||
"փո", "po",
|
||||
"քո", "qo",
|
||||
"ևո", "eo",
|
||||
"ֆո", "fo",
|
||||
"ո", "vo",
|
||||
"եւ", "ev",
|
||||
"եվ", "ev",
|
||||
"ա", "a",
|
||||
"բ", "b",
|
||||
"գ", "g",
|
||||
"դ", "d",
|
||||
"ե", "e",
|
||||
"զ", "z",
|
||||
"է", "e",
|
||||
"ը", "y",
|
||||
"թ", "t",
|
||||
"ժ", "j",
|
||||
"ի", "i",
|
||||
"լ", "l",
|
||||
"խ", "x",
|
||||
"ծ", "c",
|
||||
"կ", "k",
|
||||
"հ", "h",
|
||||
"ձ", "dz",
|
||||
"ղ", "x",
|
||||
"ճ", "c",
|
||||
"մ", "m",
|
||||
"յ", "y",
|
||||
"ն", "n",
|
||||
"շ", "sh",
|
||||
"չ", "ch",
|
||||
"պ", "p",
|
||||
"ջ", "j",
|
||||
"ռ", "r",
|
||||
"ս", "s",
|
||||
"վ", "v",
|
||||
"տ", "t",
|
||||
"ր", "r",
|
||||
"ց", "c",
|
||||
"փ", "p",
|
||||
"ք", "q",
|
||||
"օ", "o",
|
||||
"և", "ev",
|
||||
"ֆ", "f",
|
||||
"ւ", "",
|
||||
}
|
||||
|
||||
func init() {
|
||||
lower := armenianMap
|
||||
for i, val := range lower {
|
||||
if i%2 == 1 {
|
||||
continue
|
||||
}
|
||||
capital := strings.Title(val)
|
||||
if capital != val {
|
||||
armenianMap = append(armenianMap, capital, strings.Title(armenianMap[i+1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (at *ArmenianTranslit) Transliterate(s string) string {
|
||||
return strings.NewReplacer(armenianMap...).Replace(s)
|
||||
}
|
||||
42
translit/chinese.go
Normal file
42
translit/chinese.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package translit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/mozillazg/go-pinyin"
|
||||
)
|
||||
|
||||
// ChineseTranslit implements Transliterator using a pinyin
|
||||
// conversion library.
|
||||
type ChineseTranslit struct{}
|
||||
|
||||
func (ct *ChineseTranslit) Transliterate(s string) string {
|
||||
// Create buffer for final output
|
||||
outBuf := &bytes.Buffer{}
|
||||
// Create buffer to temporarily store chinese characters
|
||||
tmpBuf := &bytes.Buffer{}
|
||||
// For every character in string
|
||||
for _, char := range s {
|
||||
// If character in Han range
|
||||
if unicode.Is(unicode.Han, char) {
|
||||
// Write character to temporary buffer
|
||||
tmpBuf.WriteRune(char)
|
||||
} else {
|
||||
// If buffer contains characters
|
||||
if tmpBuf.Len() > 0 {
|
||||
// Convert to pinyin (without tones)
|
||||
out := pinyin.LazyConvert(tmpBuf.String(), nil)
|
||||
// Write space-separated string to output
|
||||
outBuf.WriteString(strings.Join(out, " "))
|
||||
// Reset temporary buffer
|
||||
tmpBuf.Reset()
|
||||
}
|
||||
// Write character to output
|
||||
outBuf.WriteRune(char)
|
||||
}
|
||||
}
|
||||
// Return output string
|
||||
return outBuf.String()
|
||||
}
|
||||
470
translit/korean.go
Normal file
470
translit/korean.go
Normal file
@@ -0,0 +1,470 @@
|
||||
package translit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
// https://en.wikipedia.org/wiki/Hangul_Jamo_%28Unicode_block%29
|
||||
var jamoBlock = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{{
|
||||
Lo: 0x1100,
|
||||
Hi: 0x11FF,
|
||||
Stride: 1,
|
||||
}},
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Hangul_Syllables
|
||||
var syllablesBlock = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{{
|
||||
Lo: 0xAC00,
|
||||
Hi: 0xD7A3,
|
||||
Stride: 1,
|
||||
}},
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Hangul_Compatibility_Jamo
|
||||
var compatJamoBlock = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{{
|
||||
Lo: 0x3131,
|
||||
Hi: 0x318E,
|
||||
Stride: 1,
|
||||
}},
|
||||
}
|
||||
|
||||
// KoreanTranslit implements transliteration for Korean.
|
||||
//
|
||||
// This was translated to Go from the code in https://codeberg.org/Freeyourgadget/Gadgetbridge
|
||||
type KoreanTranslit struct{}
|
||||
|
||||
// User input consisting of isolated jamo is usually mapped to the KS X 1001 compatibility
|
||||
// block, but jamo resulting from decomposed syllables are mapped to the modern one. This
|
||||
// function maps compat jamo to modern ones where possible and returns all other characters
|
||||
// unmodified.
|
||||
//
|
||||
// https://en.wikipedia.org/wiki/Hangul_Compatibility_Jamo
|
||||
// https://en.wikipedia.org/wiki/Hangul_Jamo_%28Unicode_block%29
|
||||
func decompatJamo(jamo rune) rune {
|
||||
// KS X 1001 Hangul filler, not used in modern Unicode. A useful landmark in the
|
||||
// compatibility jamo block.
|
||||
// https://en.wikipedia.org/wiki/KS_X_1001#Hangul_Filler
|
||||
var hangulFiller rune = 0x3164
|
||||
|
||||
// Ignore characters outside compatibility jamo block
|
||||
if !unicode.In(jamo, compatJamoBlock) {
|
||||
return jamo
|
||||
}
|
||||
|
||||
// Vowels are contiguous, in the same order, and unambiguous so it's a simple offset.
|
||||
if jamo >= 0x314F && jamo < hangulFiller {
|
||||
return jamo - 0x1FEE
|
||||
}
|
||||
|
||||
// Consonants are organized differently. No clean way to do this.
|
||||
// The compatibility jamo block doesn't distinguish between Choseong (leading) and Jongseong
|
||||
// (final) positions, but the modern block does. We map to Choseong here.
|
||||
switch jamo {
|
||||
case 0x3131:
|
||||
return 0x1100 // ㄱ
|
||||
case 0x3132:
|
||||
return 0x1101 // ㄲ
|
||||
case 0x3134:
|
||||
return 0x1102 // ㄴ
|
||||
case 0x3137:
|
||||
return 0x1103 // ㄷ
|
||||
case 0x3138:
|
||||
return 0x1104 // ㄸ
|
||||
case 0x3139:
|
||||
return 0x1105 // ㄹ
|
||||
case 0x3141:
|
||||
return 0x1106 // ㅁ
|
||||
case 0x3142:
|
||||
return 0x1107 // ㅂ
|
||||
case 0x3143:
|
||||
return 0x1108 // ㅃ
|
||||
case 0x3145:
|
||||
return 0x1109 // ㅅ
|
||||
case 0x3146:
|
||||
return 0x110A // ㅆ
|
||||
case 0x3147:
|
||||
return 0x110B // ㅇ
|
||||
case 0x3148:
|
||||
return 0x110C // ㅈ
|
||||
case 0x3149:
|
||||
return 0x110D // ㅉ
|
||||
case 0x314A:
|
||||
return 0x110E // ㅊ
|
||||
case 0x314B:
|
||||
return 0x110F // ㅋ
|
||||
case 0x314C:
|
||||
return 0x1110 // ㅌ
|
||||
case 0x314D:
|
||||
return 0x1111 // ㅍ
|
||||
case 0x314E:
|
||||
return 0x1112 // ㅎ
|
||||
}
|
||||
|
||||
// The rest of the compatibility block consists of archaic compounds that are
|
||||
// unlikely to be encountered in modern systems. Just leave them alone.
|
||||
return jamo
|
||||
}
|
||||
|
||||
// Transliterates one jamo at a time.
|
||||
// Does nothing if it isn't in the modern jamo block.
|
||||
func translitSingleJamo(jamo rune) string {
|
||||
jamo = decompatJamo(jamo)
|
||||
|
||||
switch jamo {
|
||||
// Choseong (leading position consonants)
|
||||
case 0x1100:
|
||||
return "g" // ㄱ
|
||||
case 0x1101:
|
||||
return "kk" // ㄲ
|
||||
case 0x1102:
|
||||
return "n" // ㄴ
|
||||
case 0x1103:
|
||||
return "d" // ㄷ
|
||||
case 0x1104:
|
||||
return "tt" // ㄸ
|
||||
case 0x1105:
|
||||
return "r" // ㄹ
|
||||
case 0x1106:
|
||||
return "m" // ㅁ
|
||||
case 0x1107:
|
||||
return "b" // ㅂ
|
||||
case 0x1108:
|
||||
return "pp" // ㅃ
|
||||
case 0x1109:
|
||||
return "s" // ㅅ
|
||||
case 0x110A:
|
||||
return "ss" // ㅆ
|
||||
case 0x110B:
|
||||
return "" // ㅇ
|
||||
case 0x110C:
|
||||
return "j" // ㅈ
|
||||
case 0x110D:
|
||||
return "jj" // ㅉ
|
||||
case 0x110E:
|
||||
return "ch" // ㅊ
|
||||
case 0x110F:
|
||||
return "k" // ㅋ
|
||||
case 0x1110:
|
||||
return "t" // ㅌ
|
||||
case 0x1111:
|
||||
return "p" // ㅍ
|
||||
case 0x1112:
|
||||
return "h" // ㅎ
|
||||
// Jungseong (vowels)
|
||||
case 0x1161:
|
||||
return "a" // ㅏ
|
||||
case 0x1162:
|
||||
return "ae" // ㅐ
|
||||
case 0x1163:
|
||||
return "ya" // ㅑ
|
||||
case 0x1164:
|
||||
return "yae" // ㅒ
|
||||
case 0x1165:
|
||||
return "eo" // ㅓ
|
||||
case 0x1166:
|
||||
return "e" // ㅔ
|
||||
case 0x1167:
|
||||
return "yeo" // ㅕ
|
||||
case 0x1168:
|
||||
return "ye" // ㅖ
|
||||
case 0x1169:
|
||||
return "o" // ㅗ
|
||||
case 0x116A:
|
||||
return "wa" // ㅘ
|
||||
case 0x116B:
|
||||
return "wae" // ㅙ
|
||||
case 0x116C:
|
||||
return "oe" // ㅚ
|
||||
case 0x116D:
|
||||
return "yo" // ㅛ
|
||||
case 0x116E:
|
||||
return "u" // ㅜ
|
||||
case 0x116F:
|
||||
return "wo" // ㅝ
|
||||
case 0x1170:
|
||||
return "we" // ㅞ
|
||||
case 0x1171:
|
||||
return "wi" // ㅟ
|
||||
case 0x1172:
|
||||
return "yu" // ㅠ
|
||||
case 0x1173:
|
||||
return "eu" // ㅡ
|
||||
case 0x1174:
|
||||
return "ui" // ㅢ
|
||||
case 0x1175:
|
||||
return "i" // ㅣ
|
||||
// Jongseong (final position consonants)
|
||||
case 0x11A8:
|
||||
return "k" // ㄱ
|
||||
case 0x11A9:
|
||||
return "k" // ㄲ
|
||||
case 0x11AB:
|
||||
return "n" // ㄴ
|
||||
case 0x11AE:
|
||||
return "t" // ㄷ
|
||||
case 0x11AF:
|
||||
return "l" // ㄹ
|
||||
case 0x11B7:
|
||||
return "m" // ㅁ
|
||||
case 0x11B8:
|
||||
return "p" // ㅂ
|
||||
case 0x11BA:
|
||||
return "t" // ㅅ
|
||||
case 0x11BB:
|
||||
return "t" // ㅆ
|
||||
case 0x11BC:
|
||||
return "ng" // ㅇ
|
||||
case 0x11BD:
|
||||
return "t" // ㅈ
|
||||
case 0x11BE:
|
||||
return "t" // ㅊ
|
||||
case 0x11BF:
|
||||
return "k" // ㅋ
|
||||
case 0x11C0:
|
||||
return "t" // ㅌ
|
||||
case 0x11C1:
|
||||
return "p" // ㅍ
|
||||
case 0x11C2:
|
||||
return "t" // ㅎ
|
||||
}
|
||||
|
||||
return string(jamo)
|
||||
}
|
||||
|
||||
// Some combinations of ending jamo in one syllable and initial jamo in the next are romanized
|
||||
// irregularly. These exceptions are called "special provisions". In cases where multiple
|
||||
// romanizations are permitted, we use the one that's least commonly used elsewhere.
|
||||
//
|
||||
// Returns empty strring and false if either character is not in the modern jamo block,
|
||||
// or if there is no special provision for that pair of jamo.
|
||||
func translitSpecialProvisions(previousEnding rune, nextInitial rune) (string, bool) {
|
||||
// Return false if previousEnding not in modern jamo block
|
||||
if !unicode.In(previousEnding, jamoBlock) {
|
||||
return "", false
|
||||
}
|
||||
// Return false if nextInitial not in modern jamo block
|
||||
if !unicode.In(nextInitial, jamoBlock) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Jongseong (final position) ㅎ has a number of special provisions.
|
||||
if previousEnding == 0x11C2 {
|
||||
switch nextInitial {
|
||||
case 0x110B:
|
||||
return "h", true // ㅇ
|
||||
case 0x1100:
|
||||
return "k", true // ㄱ
|
||||
case 0x1102:
|
||||
return "nn", true // ㄴ
|
||||
case 0x1103:
|
||||
return "t", true // ㄷ
|
||||
case 0x1105:
|
||||
return "nn", true // ㄹ
|
||||
case 0x1106:
|
||||
return "nm", true // ㅁ
|
||||
case 0x1107:
|
||||
return "p", true // ㅂ
|
||||
case 0x1109:
|
||||
return "hs", true // ㅅ
|
||||
case 0x110C:
|
||||
return "ch", true // ㅈ
|
||||
case 0x1112:
|
||||
return "t", true // ㅎ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, special provisions are denser when grouped by the second jamo.
|
||||
switch nextInitial {
|
||||
case 0x1100: // ㄱ
|
||||
switch previousEnding {
|
||||
case 0x11AB:
|
||||
return "n-g", true // ㄴ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
case 0x1102: // ㄴ
|
||||
switch previousEnding {
|
||||
case 0x11A8:
|
||||
return "ngn", true // ㄱ
|
||||
case 0x11AE:
|
||||
fallthrough // ㄷ
|
||||
case 0x11BA:
|
||||
fallthrough // ㅅ
|
||||
case 0x11BD:
|
||||
fallthrough // ㅈ
|
||||
case 0x11BE:
|
||||
fallthrough // ㅊ
|
||||
case 0x11C0: // ㅌ
|
||||
return "nn", true
|
||||
case 0x11AF:
|
||||
return "ll", true // ㄹ
|
||||
case 0x11B8:
|
||||
return "mn", true // ㅂ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
case 0x1105: // ㄹ
|
||||
switch previousEnding {
|
||||
case 0x11A8:
|
||||
fallthrough // ㄱ
|
||||
case 0x11AB:
|
||||
fallthrough // ㄴ
|
||||
case 0x11AF: // ㄹ
|
||||
return "ll", true
|
||||
case 0x11AE:
|
||||
fallthrough // ㄷ
|
||||
case 0x11BA:
|
||||
fallthrough // ㅅ
|
||||
case 0x11BD:
|
||||
fallthrough // ㅈ
|
||||
case 0x11BE:
|
||||
fallthrough // ㅊ
|
||||
case 0x11C0: // ㅌ
|
||||
return "nn", true
|
||||
case 0x11B7:
|
||||
fallthrough // ㅁ
|
||||
case 0x11B8: // ㅂ
|
||||
return "mn", true
|
||||
case 0x11BC:
|
||||
return "ngn", true // ㅇ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
case 0x1106: // ㅁ
|
||||
switch previousEnding {
|
||||
case 0x11A8:
|
||||
return "ngm", true // ㄱ
|
||||
case 0x11AE:
|
||||
fallthrough // ㄷ
|
||||
case 0x11BA:
|
||||
fallthrough // ㅅ
|
||||
case 0x11BD:
|
||||
fallthrough // ㅈ
|
||||
case 0x11BE:
|
||||
fallthrough // ㅊ
|
||||
case 0x11C0: // ㅌ
|
||||
return "nm", true
|
||||
case 0x11B8:
|
||||
return "mm", true // ㅂ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
case 0x110B: // ㅇ
|
||||
switch previousEnding {
|
||||
case 0x11A8:
|
||||
return "g", true // ㄱ
|
||||
case 0x11AE:
|
||||
return "d", true // ㄷ
|
||||
case 0x11AF:
|
||||
return "r", true // ㄹ
|
||||
case 0x11B8:
|
||||
return "b", true // ㅂ
|
||||
case 0x11BA:
|
||||
return "s", true // ㅅ
|
||||
case 0x11BC:
|
||||
return "ng-", true // ㅇ
|
||||
case 0x11BD:
|
||||
return "j", true // ㅈ
|
||||
case 0x11BE:
|
||||
return "ch", true // ㅊ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
case 0x110F: // ㅋ
|
||||
switch previousEnding {
|
||||
case 0x11A8:
|
||||
return "k-k", true // ㄱ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
case 0x1110: // ㅌ
|
||||
switch previousEnding {
|
||||
case 0x11AE:
|
||||
fallthrough // ㄷ
|
||||
case 0x11BA:
|
||||
fallthrough // ㅅ
|
||||
case 0x11BD:
|
||||
fallthrough // ㅈ
|
||||
case 0x11BE:
|
||||
fallthrough // ㅊ
|
||||
case 0x11C0: // ㅌ
|
||||
return "t-t", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
case 0x1111: // ㅍ
|
||||
switch previousEnding {
|
||||
case 0x11B8:
|
||||
return "p-p", true // ㅂ
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// Decompose a syllable into several jamo. Does nothing if that isn't possible.
|
||||
func decompose(syllable rune) string {
|
||||
return norm.NFD.String(string(syllable))
|
||||
}
|
||||
|
||||
// Transliterate any Hangul in the given string.
|
||||
// Leaves any non-Hangul characters unmodified.
|
||||
func (kt *KoreanTranslit) Transliterate(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
builder := &strings.Builder{}
|
||||
|
||||
nextInitialJamoConsumed := false
|
||||
|
||||
for i, syllable := range s {
|
||||
// If character not in blocks, leave it unmodified
|
||||
if !unicode.In(syllable, jamoBlock, syllablesBlock, compatJamoBlock) {
|
||||
builder.WriteRune(syllable)
|
||||
continue
|
||||
}
|
||||
|
||||
jamo := decompose(syllable)
|
||||
for j, char := range jamo {
|
||||
// If we already transliterated the first jamo of this syllable as part of a special
|
||||
// provision, skip it. Otherwise, handle it in the unconditional else branch.
|
||||
if j == 0 && nextInitialJamoConsumed {
|
||||
nextInitialJamoConsumed = false
|
||||
continue
|
||||
}
|
||||
|
||||
// If this is the last jamo of this syllable and not the last syllable of the
|
||||
// string, check for special provisions. If the next char is whitespace or not
|
||||
// Hangul, run translitSpecialProvisions() should return no value.
|
||||
if j == len(jamo)-1 && i < len(s)-1 {
|
||||
nextSyllable := s[i+1]
|
||||
nextJamo := decompose(rune(nextSyllable))[0]
|
||||
|
||||
// Attempt to handle special provision
|
||||
specialProvision, ok := translitSpecialProvisions(char, rune(nextJamo))
|
||||
if ok {
|
||||
builder.WriteString(specialProvision)
|
||||
nextInitialJamoConsumed = true
|
||||
} else {
|
||||
// Not a special provision, transliterate normally
|
||||
builder.WriteString(translitSingleJamo(char))
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Transliterate normally
|
||||
builder.WriteString(translitSingleJamo(char))
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
357
translit/translit.go
Normal file
357
translit/translit.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package translit
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Transliterate runs the given maps on s and returns the result
|
||||
func Transliterate(s string, useMaps ...string) string {
|
||||
// Create variable to store modified string
|
||||
out := s
|
||||
// If custom map exists
|
||||
if customMap, ok := Transliterators["custom"]; ok {
|
||||
// Perform transliteration with it
|
||||
out = customMap.Transliterate(out)
|
||||
}
|
||||
// For every map to use
|
||||
for _, useMap := range useMaps {
|
||||
// If custom, skip
|
||||
if useMap == "custom" {
|
||||
continue
|
||||
}
|
||||
// Get requested map
|
||||
translitMap, ok := Transliterators[useMap]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Perform transliteration
|
||||
out = translitMap.Transliterate(out)
|
||||
}
|
||||
// Return result
|
||||
return out
|
||||
}
|
||||
|
||||
// Transliterator is implemented by anything with a
|
||||
// Transliterate method, which performs transliteration
|
||||
// and returns the resulting string.
|
||||
type Transliterator interface {
|
||||
Transliterate(string) string
|
||||
}
|
||||
|
||||
// Map implements Transliterator using a slice where
|
||||
// every odd element is a key and every even one is a value
|
||||
// which replaces the key.
|
||||
type Map []string
|
||||
|
||||
func (mt Map) Transliterate(s string) string {
|
||||
return strings.NewReplacer(mt...).Replace(s)
|
||||
}
|
||||
|
||||
// Transliterators stores transliterator implementations for each supported language.
|
||||
// Some of these were sourced from https://codeberg.org/Freeyourgadget/Gadgetbridge
|
||||
var Transliterators = map[string]Transliterator{
|
||||
"eASCII": Map{
|
||||
"œ", "oe",
|
||||
"ª", "a",
|
||||
"°", "o",
|
||||
"«", `"`,
|
||||
"»", `"`,
|
||||
},
|
||||
"Scandinavian": Map{
|
||||
"Æ", "Ae",
|
||||
"æ", "ae",
|
||||
"Ø", "Oe",
|
||||
"ø", "oe",
|
||||
"Å", "Aa",
|
||||
"å", "aa",
|
||||
},
|
||||
"German": Map{
|
||||
"ä", "ae",
|
||||
"ö", "oe",
|
||||
"ü", "ue",
|
||||
"Ä", "Ae",
|
||||
"Ö", "Oe",
|
||||
"Ü", "Ue",
|
||||
"ß", "ss",
|
||||
"ẞ", "SS",
|
||||
},
|
||||
"Hebrew": Map{
|
||||
"א", "a",
|
||||
"ב", "b",
|
||||
"ג", "g",
|
||||
"ד", "d",
|
||||
"ה", "h",
|
||||
"ו", "u",
|
||||
"ז", "z",
|
||||
"ח", "kh",
|
||||
"ט", "t",
|
||||
"י", "y",
|
||||
"כ", "c",
|
||||
"ל", "l",
|
||||
"מ", "m",
|
||||
"נ", "n",
|
||||
"ס", "s",
|
||||
"ע", "'",
|
||||
"פ", "p",
|
||||
"צ", "ts",
|
||||
"ק", "k",
|
||||
"ר", "r",
|
||||
"ש", "sh",
|
||||
"ת", "th",
|
||||
"ף", "f",
|
||||
"ץ", "ts",
|
||||
"ך", "ch",
|
||||
"ם", "m",
|
||||
"ן", "n",
|
||||
},
|
||||
"Greek": Map{
|
||||
"α", "a",
|
||||
"ά", "a",
|
||||
"β", "v",
|
||||
"γ", "g",
|
||||
"δ", "d",
|
||||
"ε", "e",
|
||||
"έ", "e",
|
||||
"ζ", "z",
|
||||
"η", "i",
|
||||
"ή", "i",
|
||||
"θ", "th",
|
||||
"ι", "i",
|
||||
"ί", "i",
|
||||
"ϊ", "i",
|
||||
"ΐ", "i",
|
||||
"κ", "k",
|
||||
"λ", "l",
|
||||
"μ", "m",
|
||||
"ν", "n",
|
||||
"ξ", "ks",
|
||||
"ο", "o",
|
||||
"ό", "o",
|
||||
"π", "p",
|
||||
"ρ", "r",
|
||||
"σ", "s",
|
||||
"ς", "s",
|
||||
"τ", "t",
|
||||
"υ", "y",
|
||||
"ύ", "y",
|
||||
"ϋ", "y",
|
||||
"ΰ", "y",
|
||||
"φ", "f",
|
||||
"χ", "ch",
|
||||
"ψ", "ps",
|
||||
"ω", "o",
|
||||
"ώ", "o",
|
||||
"Α", "A",
|
||||
"Ά", "A",
|
||||
"Β", "B",
|
||||
"Γ", "G",
|
||||
"Δ", "D",
|
||||
"Ε", "E",
|
||||
"Έ", "E",
|
||||
"Ζ", "Z",
|
||||
"Η", "I",
|
||||
"Ή", "I",
|
||||
"Θ", "TH",
|
||||
"Ι", "I",
|
||||
"Ί", "I",
|
||||
"Ϊ", "I",
|
||||
"Κ", "K",
|
||||
"Λ", "L",
|
||||
"Μ", "M",
|
||||
"Ν", "N",
|
||||
"Ξ", "KS",
|
||||
"Ο", "O",
|
||||
"Ό", "O",
|
||||
"Π", "P",
|
||||
"Ρ", "R",
|
||||
"Σ", "S",
|
||||
"Τ", "T",
|
||||
"Υ", "Y",
|
||||
"Ύ", "Y",
|
||||
"Ϋ", "Y",
|
||||
"Φ", "F",
|
||||
"Χ", "CH",
|
||||
"Ψ", "PS",
|
||||
"Ω", "O",
|
||||
"Ώ", "O",
|
||||
},
|
||||
"Russian": Map{
|
||||
"Ё", "Йo",
|
||||
"ё", "йo",
|
||||
},
|
||||
"Ukranian": Map{
|
||||
"ґ", "gh",
|
||||
"є", "je",
|
||||
"і", "i",
|
||||
"ї", "ji",
|
||||
"Ґ", "GH",
|
||||
"Є", "JE",
|
||||
"І", "I",
|
||||
"Ї", "JI",
|
||||
},
|
||||
"Arabic": Map{
|
||||
"ا", "a",
|
||||
"ب", "b",
|
||||
"ت", "t",
|
||||
"ث", "th",
|
||||
"ج", "j",
|
||||
"ح", "7",
|
||||
"خ", "5",
|
||||
"د", "d",
|
||||
"ذ", "th",
|
||||
"ر", "r",
|
||||
"ز", "z",
|
||||
"س", "s",
|
||||
"ش", "sh",
|
||||
"ص", "9",
|
||||
"ض", "9'",
|
||||
"ط", "6",
|
||||
"ظ", "6'",
|
||||
"ع", "3",
|
||||
"غ", "3'",
|
||||
"ف", "f",
|
||||
"ق", "q",
|
||||
"ك", "k",
|
||||
"ل", "l",
|
||||
"م", "m",
|
||||
"ن", "n",
|
||||
"ه", "h",
|
||||
"و", "w",
|
||||
"ي", "y",
|
||||
"ى", "a",
|
||||
"ﺓ", "",
|
||||
"آ", "2",
|
||||
"ئ", "2",
|
||||
"إ", "2",
|
||||
"ؤ", "2",
|
||||
"أ", "2",
|
||||
"ء", "2",
|
||||
"٠", "0",
|
||||
"١", "1",
|
||||
"٢", "2",
|
||||
"٣", "3",
|
||||
"٤", "4",
|
||||
"٥", "5",
|
||||
"٦", "6",
|
||||
"٧", "7",
|
||||
"٨", "8",
|
||||
"٩", "9",
|
||||
},
|
||||
"Farsi": Map{
|
||||
"پ", "p",
|
||||
"چ", "ch",
|
||||
"ژ", "zh",
|
||||
"ک", "k",
|
||||
"گ", "g",
|
||||
"ی", "y",
|
||||
"\u200c", " ",
|
||||
"؟", "?",
|
||||
"٪", "%",
|
||||
"؛", ";",
|
||||
"،", ":",
|
||||
"۱", "1",
|
||||
"۲", "2",
|
||||
"۳", "3",
|
||||
"۴", "4",
|
||||
"۵", "5",
|
||||
"۶", "6",
|
||||
"۷", "7",
|
||||
"۸", "8",
|
||||
"۹", "9",
|
||||
"۰", "0",
|
||||
"»", "<",
|
||||
"«", ">",
|
||||
"ِ", "e",
|
||||
"َ", "a",
|
||||
"ُ", "o",
|
||||
"ّ", "",
|
||||
},
|
||||
"Polish": Map{
|
||||
"Ł", "L",
|
||||
"ł", "l",
|
||||
},
|
||||
"Lithuanian": Map{
|
||||
"ą", "a",
|
||||
"č", "c",
|
||||
"ę", "e",
|
||||
"ė", "e",
|
||||
"į", "i",
|
||||
"š", "s",
|
||||
"ų", "u",
|
||||
"ū", "u",
|
||||
"ž", "z",
|
||||
},
|
||||
"Estonian": Map{
|
||||
"ä", "a",
|
||||
"Ä", "A",
|
||||
"ö", "o",
|
||||
"õ", "o",
|
||||
"Ö", "O",
|
||||
"Õ", "O",
|
||||
"ü", "u",
|
||||
"Ü", "U",
|
||||
},
|
||||
"Icelandic": Map{
|
||||
"Þ", "Th",
|
||||
"þ", "th",
|
||||
"Ð", "D",
|
||||
"ð", "d",
|
||||
},
|
||||
"Czeck": Map{
|
||||
"ř", "r",
|
||||
"ě", "e",
|
||||
"ý", "y",
|
||||
"á", "a",
|
||||
"í", "i",
|
||||
"é", "e",
|
||||
"ó", "o",
|
||||
"ú", "u",
|
||||
"ů", "u",
|
||||
"ď", "d",
|
||||
"ť", "t",
|
||||
"ň", "n",
|
||||
},
|
||||
"French": Map{
|
||||
"à", "a",
|
||||
"â", "a",
|
||||
"é", "e",
|
||||
"è", "e",
|
||||
"ê", "e",
|
||||
"ë", "e",
|
||||
"ù", "u",
|
||||
"ü", "u",
|
||||
"ÿ", "y",
|
||||
"ç", "c",
|
||||
},
|
||||
"Emoji": Map{
|
||||
"😂", ":')",
|
||||
"😊", ":)",
|
||||
"😃", ":)",
|
||||
"😩", "-_-",
|
||||
"😏", ":‑J",
|
||||
"💜", "<3",
|
||||
"💖", "<3",
|
||||
"💗", "<3",
|
||||
"❤️", "<3",
|
||||
"💕", "<3",
|
||||
"💞", "<3",
|
||||
"💘", "<3",
|
||||
"💓", "<3",
|
||||
"💚", "<3",
|
||||
"💙", "<3",
|
||||
"💔", "</3",
|
||||
"😱", "D:",
|
||||
"😮", ":O",
|
||||
"😝", ":P",
|
||||
"😍", ":x",
|
||||
"😢", ":(",
|
||||
"💯", ":100:",
|
||||
"🔥", ":fire:",
|
||||
"😉", ";)",
|
||||
"😴", ":zzz:",
|
||||
"💤", ":zzz:",
|
||||
},
|
||||
"Korean": &KoreanTranslit{},
|
||||
"Chinese": &ChineseTranslit{},
|
||||
"Armenian": &ArmenianTranslit{},
|
||||
}
|
||||
Reference in New Issue
Block a user