Added basic i18n
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,3 +3,6 @@
|
|||||||
*.csv
|
*.csv
|
||||||
*.json
|
*.json
|
||||||
*.swp
|
*.swp
|
||||||
|
.env
|
||||||
|
golifehk
|
||||||
|
cmd/compilei18n/compliei18n
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package tg
|
package tg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
query "github.com/tgckpg/golifehk/query"
|
query "github.com/tgckpg/golifehk/query"
|
||||||
)
|
)
|
||||||
@@ -56,7 +55,6 @@ func BotSend(bot *tgbotapi.BotAPI, update *tgbotapi.Update, qResult query.IQuery
|
|||||||
for _, cell := range row {
|
for _, cell := range row {
|
||||||
button := tgbotapi.NewInlineKeyboardButtonData(cell.Name, cell.Value)
|
button := tgbotapi.NewInlineKeyboardButtonData(cell.Name, cell.Value)
|
||||||
buttons = append(buttons, button)
|
buttons = append(buttons, button)
|
||||||
fmt.Println(cell)
|
|
||||||
}
|
}
|
||||||
buttonRows = append(buttonRows, buttons)
|
buttonRows = append(buttonRows, buttons)
|
||||||
}
|
}
|
||||||
|
|||||||
3
cmd/compilei18n/go.mod
Normal file
3
cmd/compilei18n/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module chatgpt.com/c/tools/compliei18n
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
121
cmd/compilei18n/main.go
Normal file
121
cmd/compilei18n/main.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const goFile = "i18n/messages.go"
|
||||||
|
const outFile = "resources/src/messages.json"
|
||||||
|
|
||||||
|
keys, err := extractStringConsts(goFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
existing := map[string]string{}
|
||||||
|
if data, err := os.ReadFile(outFile); err == nil {
|
||||||
|
if len(bytes.TrimSpace(data)) > 0 {
|
||||||
|
if err := json.Unmarshal(data, &existing); err != nil {
|
||||||
|
panic(fmt.Errorf("parse %s: %w", outFile, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := map[string]string{}
|
||||||
|
for _, key := range keys {
|
||||||
|
if val, ok := existing[key]; ok {
|
||||||
|
merged[key] = val
|
||||||
|
} else {
|
||||||
|
merged[key] = "TODO: " + key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect stale keys.
|
||||||
|
for key := range existing {
|
||||||
|
found := false
|
||||||
|
for _, k := range keys {
|
||||||
|
if k == key {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
fmt.Fprintf(os.Stderr, "stale key in %s: %s\n", outFile, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write stable sorted JSON.
|
||||||
|
sorted := make([]string, 0, len(merged))
|
||||||
|
for k := range merged {
|
||||||
|
sorted = append(sorted, k)
|
||||||
|
}
|
||||||
|
sort.Strings(sorted)
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
buf.WriteString("{\n")
|
||||||
|
for i, k := range sorted {
|
||||||
|
kj, _ := json.Marshal(k)
|
||||||
|
vj, _ := json.Marshal(merged[k])
|
||||||
|
comma := ","
|
||||||
|
if i == len(sorted)-1 {
|
||||||
|
comma = ""
|
||||||
|
}
|
||||||
|
fmt.Fprintf(buf, " %s: %s%s\n", kj, vj, comma)
|
||||||
|
}
|
||||||
|
buf.WriteString("}\n")
|
||||||
|
|
||||||
|
if err := os.WriteFile(outFile, buf.Bytes(), 0644); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractStringConsts(path string) ([]string, error) {
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
file, err := parser.ParseFile(fset, path, nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
|
||||||
|
for _, decl := range file.Decls {
|
||||||
|
gen, ok := decl.(*ast.GenDecl)
|
||||||
|
if !ok || gen.Tok != token.CONST {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spec := range gen.Specs {
|
||||||
|
vs, ok := spec.(*ast.ValueSpec)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range vs.Values {
|
||||||
|
lit, ok := value.(*ast.BasicLit)
|
||||||
|
if !ok || lit.Kind != token.STRING {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := strconv.Unquote(lit.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keys = append(keys, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
9
cmd/po-build.sh
Executable file
9
cmd/po-build.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
RES_ROOT=./resources
|
||||||
|
|
||||||
|
# Primary language
|
||||||
|
cp -r "$RES_ROOT/src" "$RES_ROOT/langpacks/en"
|
||||||
|
|
||||||
|
# zh-Hant
|
||||||
|
po2json -t "$RES_ROOT/src/" "$RES_ROOT/po/zh-Hant" "$RES_ROOT/langpacks/zh-Hant"
|
||||||
7
cmd/po-export.sh
Executable file
7
cmd/po-export.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
RES_ROOT=./resources
|
||||||
|
|
||||||
|
./cmd/compilei18n/compliei18n
|
||||||
|
json2po -P "$RES_ROOT/src/" "$RES_ROOT/pot/"
|
||||||
|
pot2po -t "$RES_ROOT/po/zh-Hant" "$RES_ROOT/pot" "$RES_ROOT/po/zh-Hant"
|
||||||
@@ -2,10 +2,12 @@ package kmb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
i18n "github.com/tgckpg/golifehk/i18n"
|
||||||
query "github.com/tgckpg/golifehk/query"
|
query "github.com/tgckpg/golifehk/query"
|
||||||
utils "github.com/tgckpg/golifehk/utils"
|
utils "github.com/tgckpg/golifehk/utils"
|
||||||
)
|
)
|
||||||
@@ -19,6 +21,9 @@ type QueryResult struct {
|
|||||||
isConsumed bool
|
isConsumed bool
|
||||||
dataType string
|
dataType string
|
||||||
tableData [][]query.TableCell
|
tableData [][]query.TableCell
|
||||||
|
|
||||||
|
FallbackNearest int
|
||||||
|
NearestRange float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRouteHead(sb *strings.Builder, r *RouteStop) {
|
func writeRouteHead(sb *strings.Builder, r *RouteStop) {
|
||||||
@@ -64,6 +69,11 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
return "", this.Error
|
return "", this.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
langPack, err := i18n.LoadKeys(this.Lang)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
sb := strings.Builder{}
|
sb := strings.Builder{}
|
||||||
|
|
||||||
if 0 < len(*this.Query.Results) {
|
if 0 < len(*this.Query.Results) {
|
||||||
@@ -72,23 +82,38 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
if this.Query.Key == "" {
|
if this.Query.Key == "" {
|
||||||
loc := this.Query.Message.Location
|
loc := this.Query.Message.Location
|
||||||
if loc != nil {
|
if loc != nil {
|
||||||
sb.WriteString("九巴 100m")
|
|
||||||
this.dataType = "Table"
|
this.dataType = "Table"
|
||||||
|
|
||||||
table := [][]query.TableCell{}
|
table := [][]query.TableCell{}
|
||||||
|
minDist := math.MaxFloat64
|
||||||
|
maxDist := -1.0
|
||||||
|
|
||||||
for _, item := range *this.Query.Results {
|
for _, item := range *this.Query.Results {
|
||||||
b := any(item).(*BusStop)
|
b := any(item).(*BusStop)
|
||||||
|
|
||||||
row := []query.TableCell{}
|
row := []query.TableCell{}
|
||||||
|
|
||||||
|
bDist := b.Dist(loc.Lat(), loc.Lon())
|
||||||
|
if bDist < minDist {
|
||||||
|
minDist = bDist
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxDist < bDist {
|
||||||
|
maxDist = bDist
|
||||||
|
}
|
||||||
|
|
||||||
cell := query.TableCell{
|
cell := query.TableCell{
|
||||||
Name: fmt.Sprintf("%.2fm %s", b.Dist(loc.Lat(), loc.Lon()), (*b.Name)[this.Lang]),
|
Name: fmt.Sprintf("%s (%s)", (*b.Name)[this.Lang], i18n.FormatDistance(langPack, bDist)),
|
||||||
Value: fmt.Sprintf("%s", b.BusStopId),
|
Value: fmt.Sprintf("%s", b.BusStopId),
|
||||||
}
|
}
|
||||||
row = append(row, cell)
|
row = append(row, cell)
|
||||||
|
|
||||||
for _, r := range *b.Routes {
|
for colIndex, r := range *b.Routes {
|
||||||
|
if colIndex%6 == 0 {
|
||||||
|
table = append(table, row)
|
||||||
|
row = []query.TableCell{}
|
||||||
|
}
|
||||||
sb_i := strings.Builder{}
|
sb_i := strings.Builder{}
|
||||||
writeRouteHead(&sb_i, r)
|
writeRouteHead(&sb_i, r)
|
||||||
cell := query.TableCell{
|
cell := query.TableCell{
|
||||||
@@ -102,6 +127,13 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
}
|
}
|
||||||
this.tableData = table
|
this.tableData = table
|
||||||
|
|
||||||
|
rangeText := i18n.FormatDistance(langPack, this.NearestRange)
|
||||||
|
if maxDist < this.NearestRange {
|
||||||
|
utils.WriteMDv2Text(&sb, i18n.DS_KMB_NEAREST_STOPS.Text(langPack, rangeText))
|
||||||
|
} else if this.NearestRange < minDist {
|
||||||
|
utils.WriteMDv2Text(&sb, i18n.DS_KMB_NO_NEAREST_STOPS.Text(langPack, rangeText, this.FallbackNearest))
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
busStops := map[string]*BusStop{}
|
busStops := map[string]*BusStop{}
|
||||||
for _, item := range *this.Query.Results {
|
for _, item := range *this.Query.Results {
|
||||||
@@ -149,16 +181,13 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
_m := schedule.ETA.Sub(now).Minutes()
|
_m := schedule.ETA.Sub(now).Minutes()
|
||||||
|
|
||||||
sb.WriteString(" \\* ")
|
sb.WriteString(" \\* ")
|
||||||
txt := "%.0f min(s)"
|
|
||||||
|
|
||||||
if this.Lang == "zh-Hant" {
|
eta := i18n.UNITS_MINUTE.Text(langPack, _m)
|
||||||
txt = "%.0f 分鐘"
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.WriteMDv2Text(&sb, fmt.Sprintf(txt, _m))
|
|
||||||
|
|
||||||
if _m < 0 {
|
if _m < 0 {
|
||||||
sb.WriteString(" 走左了?")
|
utils.WriteMDv2Text(&sb, i18n.DS_KMB_ETA_DEPARTED.Text(langPack, eta))
|
||||||
|
} else {
|
||||||
|
utils.WriteMDv2Text(&sb, eta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ func Query(q query.QueryMessage) query.IQueryResult {
|
|||||||
var err error
|
var err error
|
||||||
var routeStops *[]query.ISearchable
|
var routeStops *[]query.ISearchable
|
||||||
|
|
||||||
qr := QueryResult{Lang: lang}
|
qr := QueryResult{
|
||||||
|
Lang: lang,
|
||||||
|
FallbackNearest: 3,
|
||||||
|
NearestRange: 50,
|
||||||
|
}
|
||||||
|
|
||||||
busStops, err := readBusStopsData()
|
busStops, err := readBusStopsData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -37,7 +41,7 @@ func Query(q query.QueryMessage) query.IQueryResult {
|
|||||||
bList = append(bList, b)
|
bList = append(bList, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
qo, err = query.MatchNearest(*q.Location, &bList, 100, 3)
|
qo, err = query.MatchNearest(*q.Location, &bList, qr.NearestRange, qr.FallbackNearest)
|
||||||
}
|
}
|
||||||
|
|
||||||
qo.Message = &q
|
qo.Message = &q
|
||||||
|
|||||||
@@ -2,35 +2,53 @@ package kmb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQuerySchedule(t *testing.T) {
|
func TestQuerySchedule(t *testing.T) {
|
||||||
qo := Query( "zh-Hant", "68X" )
|
qo := Query(query.QueryMessage{Lang: "zh-Hant", Text: "68X"})
|
||||||
mesg, err := qo.Message()
|
mesg, err := qo.Message()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected Error: %s", err)
|
t.Errorf("Unexpected Error: %s", err)
|
||||||
}
|
}
|
||||||
fmt.Println( mesg )
|
|
||||||
|
|
||||||
qo = Query( "zh-Hant", "K66 朗屏" )
|
qo = Query(query.QueryMessage{Lang: "zh-Hant", Text: "K66 朗屏"})
|
||||||
mesg, err = qo.Message()
|
mesg, err = qo.Message()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected Error: %s, got \"\" instead", mesg)
|
t.Errorf("Expected Error: %s, got \"\" instead", mesg)
|
||||||
}
|
}
|
||||||
|
|
||||||
qo = Query( "zh-Hant", "大欖" )
|
qo = Query(query.QueryMessage{Lang: "zh-Hant", Text: "大欖"})
|
||||||
mesg, err = qo.Message()
|
mesg, err = qo.Message()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected Error: %s", err)
|
t.Errorf("Unexpected Error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println( mesg )
|
qo = Query(query.QueryMessage{Lang: "zh-Hant", Text: "261B 大欖"})
|
||||||
|
|
||||||
qo = Query( "zh-Hant", "261B 大欖" )
|
|
||||||
mesg, err = qo.Message()
|
mesg, err = qo.Message()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected Error: %s", err)
|
t.Errorf("Unexpected Error: %s", err)
|
||||||
}
|
}
|
||||||
fmt.Println( mesg )
|
|
||||||
|
qo = Query(query.QueryMessage{
|
||||||
|
Lang: "zh-Hant", Text: "",
|
||||||
|
// Yuen Long Plaza
|
||||||
|
// Location: &query.GeoLocation{22.444894482044997, 114.02393826485495},
|
||||||
|
// Nathan Rd
|
||||||
|
// Location: &query.GeoLocation{22.308944848482525, 114.17116565400259},
|
||||||
|
// GO PARK
|
||||||
|
Location: &query.GeoLocation{22.427238734660868, 114.26595846515744},
|
||||||
|
})
|
||||||
|
mesg, err = qo.Message()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range qo.GetTableData() {
|
||||||
|
for _, cell := range row {
|
||||||
|
fmt.Printf("| %s |", cell.Name)
|
||||||
|
}
|
||||||
|
fmt.Print("\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestRouteStops(t *testing.T) {
|
func TestRouteStops(t *testing.T) {
|
||||||
_, err := getRouteStops()
|
busStops, err := readBusStopsData()
|
||||||
|
_, err = getRouteStops(busStops)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package bus
|
package bus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
i18n "github.com/tgckpg/golifehk/i18n"
|
i18n "github.com/tgckpg/golifehk/i18n"
|
||||||
query "github.com/tgckpg/golifehk/query"
|
query "github.com/tgckpg/golifehk/query"
|
||||||
)
|
)
|
||||||
@@ -53,10 +54,11 @@ func (this *BusStop) Reload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (this BusStop) Register(registers map[string]struct{}) bool {
|
func (this BusStop) Register(registers map[string]struct{}) bool {
|
||||||
if _, ok := registers[this.StationId]; ok {
|
key := fmt.Sprintf("%s,%s", this.StationId, this.ReferenceId)
|
||||||
|
if _, ok := registers[key]; ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
registers[this.StationId] = struct{}{}
|
registers[key] = struct{}{}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
i18n "github.com/tgckpg/golifehk/i18n"
|
||||||
query "github.com/tgckpg/golifehk/query"
|
query "github.com/tgckpg/golifehk/query"
|
||||||
utils "github.com/tgckpg/golifehk/utils"
|
utils "github.com/tgckpg/golifehk/utils"
|
||||||
)
|
)
|
||||||
@@ -20,6 +21,9 @@ type QueryResult struct {
|
|||||||
isConsumed bool
|
isConsumed bool
|
||||||
dataType string
|
dataType string
|
||||||
tableData [][]query.TableCell
|
tableData [][]query.TableCell
|
||||||
|
|
||||||
|
FallbackNearest int
|
||||||
|
NearestRange float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) {
|
func writeShortRoute(lang *string, sb *strings.Builder, b *BusStop) {
|
||||||
@@ -52,6 +56,11 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
return "", this.Error
|
return "", this.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
langPack, err := i18n.LoadKeys(this.Lang)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
sb := strings.Builder{}
|
sb := strings.Builder{}
|
||||||
|
|
||||||
if this.Schedules == nil {
|
if this.Schedules == nil {
|
||||||
@@ -72,17 +81,41 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
if loc != nil {
|
if loc != nil {
|
||||||
|
|
||||||
this.dataType = "Table"
|
this.dataType = "Table"
|
||||||
sb.WriteString("K巴 100m")
|
|
||||||
|
|
||||||
table := [][]query.TableCell{}
|
table := [][]query.TableCell{}
|
||||||
|
|
||||||
|
// Group by Station Name first
|
||||||
|
bGroups := map[string]*[]*BusStop{}
|
||||||
|
|
||||||
for _, entry := range *q.Results {
|
for _, entry := range *q.Results {
|
||||||
busStop := any(entry).(*BusStop)
|
busStop := any(entry).(*BusStop)
|
||||||
|
bName := (*busStop.Name)[this.Lang]
|
||||||
|
bGroup, ok := bGroups[bName]
|
||||||
|
if !ok {
|
||||||
|
bGroup = &[]*BusStop{}
|
||||||
|
bGroups[bName] = bGroup
|
||||||
|
}
|
||||||
|
*bGroup = append(*bGroup, busStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
for bName, bGroup := range bGroups {
|
||||||
|
|
||||||
|
row := []query.TableCell{
|
||||||
|
query.TableCell{Name: bName, Value: bName},
|
||||||
|
}
|
||||||
|
gRow := row
|
||||||
|
|
||||||
|
var minDist float64
|
||||||
|
var maxDist float64
|
||||||
|
|
||||||
|
for colIndex, busStop := range *bGroup {
|
||||||
|
if colIndex%6 == 0 {
|
||||||
|
table = append(table, row)
|
||||||
|
row = []query.TableCell{}
|
||||||
|
}
|
||||||
|
|
||||||
sb_i := strings.Builder{}
|
sb_i := strings.Builder{}
|
||||||
sb_i.WriteString(fmt.Sprintf("%.2fm", busStop.Dist(loc.Lat(), loc.Lon())))
|
sb_i.WriteString(busStop.RouteId)
|
||||||
sb_i.WriteString(" ")
|
|
||||||
utils.WriteMDv2Text(&sb_i, busStop.RouteId)
|
|
||||||
d := busStop.Direction
|
d := busStop.Direction
|
||||||
if d == "O" {
|
if d == "O" {
|
||||||
sb_i.WriteString("↑")
|
sb_i.WriteString("↑")
|
||||||
@@ -91,14 +124,40 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
} else {
|
} else {
|
||||||
sb_i.WriteString("\\?")
|
sb_i.WriteString("\\?")
|
||||||
}
|
}
|
||||||
sb_i.WriteString(" ")
|
|
||||||
utils.WriteMDv2Text(&sb_i, (*busStop.Name)[this.Lang])
|
|
||||||
|
|
||||||
row := []query.TableCell{
|
cell := query.TableCell{
|
||||||
query.TableCell{
|
|
||||||
Name: sb_i.String(),
|
Name: sb_i.String(),
|
||||||
Value: fmt.Sprintf("%s %s", busStop.RouteId, (*busStop.Name)[this.Lang]),
|
Value: fmt.Sprintf("%s %s", busStop.RouteId, bName),
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// Data are already sorted by shortest dist
|
||||||
|
// So the first one must be min dist
|
||||||
|
if minDist == 0 {
|
||||||
|
minDist = busStop.Dist(loc.Lat(), loc.Lon())
|
||||||
|
}
|
||||||
|
|
||||||
|
if colIndex+1 == len(*bGroup) {
|
||||||
|
maxDist = busStop.Dist(loc.Lat(), loc.Lon())
|
||||||
|
}
|
||||||
|
|
||||||
|
row = append(row, cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
if minDist == maxDist {
|
||||||
|
gRow[0].Name = fmt.Sprintf("%s (%s)", bName, i18n.FormatDistance(langPack, minDist))
|
||||||
|
} else {
|
||||||
|
gRow[0].Name = fmt.Sprintf(
|
||||||
|
"%s (%s~%s)",
|
||||||
|
bName, i18n.FormatDistance(langPack, minDist),
|
||||||
|
bName, i18n.FormatDistance(langPack, maxDist),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeText := i18n.FormatDistance(langPack, this.NearestRange)
|
||||||
|
if maxDist < this.NearestRange {
|
||||||
|
utils.WriteMDv2Text(&sb, i18n.DS_MTR_NEAREST_STOPS.Text(langPack, rangeText))
|
||||||
|
} else if this.NearestRange < minDist {
|
||||||
|
utils.WriteMDv2Text(&sb, i18n.DS_MTR_NO_NEAREST_STOPS.Text(langPack, rangeText, this.FallbackNearest))
|
||||||
}
|
}
|
||||||
|
|
||||||
table = append(table, row)
|
table = append(table, row)
|
||||||
@@ -235,7 +294,7 @@ func (this *QueryResult) Message() (string, error) {
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
utils.WriteMDv2Text(&sb, "Schedules are empty...perhaps Out of Service Time?")
|
utils.WriteMDv2Text(&sb, i18n.DS_MTR_NO_SCHEDULES.Text(langPack))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
query "github.com/tgckpg/golifehk/query"
|
query "github.com/tgckpg/golifehk/query"
|
||||||
"github.com/tgckpg/golifehk/utils"
|
"github.com/tgckpg/golifehk/utils"
|
||||||
@@ -52,9 +53,9 @@ func readBusStopData(r io.Reader) (*map[string]*BusStop, error) {
|
|||||||
v, _ := strconv.ParseFloat(value, 64)
|
v, _ := strconv.ParseFloat(value, 64)
|
||||||
entry.Longitude = v
|
entry.Longitude = v
|
||||||
case "STATION_NAME_CHI":
|
case "STATION_NAME_CHI":
|
||||||
entry.Name_zh = value
|
entry.Name_zh = strings.TrimSpace(value)
|
||||||
case "STATION_NAME_ENG":
|
case "STATION_NAME_ENG":
|
||||||
entry.Name_en = value
|
entry.Name_en = strings.TrimSpace(value)
|
||||||
case "REFERENCE_ID":
|
case "REFERENCE_ID":
|
||||||
entry.ReferenceId = value
|
entry.ReferenceId = value
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ func Query(q query.QueryMessage) query.IQueryResult {
|
|||||||
var qBusStops *query.QueryObject
|
var qBusStops *query.QueryObject
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
qr := QueryResult{Lang: lang}
|
qr := QueryResult{
|
||||||
|
Lang: lang,
|
||||||
|
FallbackNearest: 3,
|
||||||
|
NearestRange: 50,
|
||||||
|
}
|
||||||
|
|
||||||
busStops, err := getBusStops()
|
busStops, err := getBusStops()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
qr.Error = err
|
qr.Error = err
|
||||||
@@ -23,16 +28,16 @@ func Query(q query.QueryMessage) query.IQueryResult {
|
|||||||
if q.Text != "" {
|
if q.Text != "" {
|
||||||
qBusStops, err = query.MatchKeys(strings.ToUpper(q.Text), busStops)
|
qBusStops, err = query.MatchKeys(strings.ToUpper(q.Text), busStops)
|
||||||
} else if q.Location != nil {
|
} else if q.Location != nil {
|
||||||
qBusStops, err = query.MatchNearest(*q.Location, busStops, 100, 3)
|
qBusStops, err = query.MatchNearest(*q.Location, busStops, qr.NearestRange, qr.FallbackNearest)
|
||||||
}
|
}
|
||||||
|
|
||||||
qBusStops.Message = &q
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
qr.Error = err
|
qr.Error = err
|
||||||
goto qrReturn
|
goto qrReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qBusStops.Message = &q
|
||||||
|
|
||||||
qr.Query = qBusStops
|
qr.Query = qBusStops
|
||||||
if 0 < len(*qBusStops.Results) && 1 < len(*qBusStops.SearchTerms) {
|
if 0 < len(*qBusStops.Results) && 1 < len(*qBusStops.SearchTerms) {
|
||||||
schedules, err := getSchedule(lang, qBusStops.Key)
|
schedules, err := getSchedule(lang, qBusStops.Key)
|
||||||
|
|||||||
@@ -2,32 +2,54 @@ package bus
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestQuery(t *testing.T) {
|
func TestQuery(t *testing.T) {
|
||||||
qo := Query( "zh-Hant", "K73" )
|
qo := Query(query.QueryMessage{Lang: "zh-Hant", Text: "K73"})
|
||||||
mesg, err := qo.Message()
|
mesg, err := qo.Message()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected Error: %s", err)
|
t.Errorf("Unexpected Error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains( mesg, "K73\\-O" ) {
|
if !strings.Contains(mesg, "K73↓") {
|
||||||
t.Errorf("Expected Route Listing, got \"%s\" instead", mesg)
|
t.Errorf("Expected Route Listing, got \"%s\" instead", mesg)
|
||||||
}
|
}
|
||||||
|
|
||||||
qo = Query( "zh-Hant", "K76 池" )
|
qo = Query(query.QueryMessage{Lang: "zh-Hant", Text: "K76 池"})
|
||||||
mesg, err = qo.Message()
|
mesg, err = qo.Message()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expecting error, got \"%s\" instead", mesg)
|
t.Errorf("Expecting error, got \"%s\" instead", mesg)
|
||||||
}
|
}
|
||||||
|
|
||||||
qo = Query( "zh-Hant", "K73 池" )
|
qo = Query(query.QueryMessage{Lang: "zh-Hant", Text: "K73 池"})
|
||||||
mesg, err = qo.Message()
|
mesg, err = qo.Message()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected Error: %s", err)
|
t.Errorf("Unexpected Error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println( mesg )
|
qo = Query(query.QueryMessage{
|
||||||
|
Lang: "zh-Hant", Text: "",
|
||||||
|
// Yuen Long Plaza
|
||||||
|
Location: &query.GeoLocation{22.444894482044997, 114.02393826485495},
|
||||||
|
// Nathan Rd
|
||||||
|
// Location: &query.GeoLocation{22.308944848482525, 114.17116565400259},
|
||||||
|
// GO PARK
|
||||||
|
// Location: &query.GeoLocation{22.427238734660868, 114.26595846515744},
|
||||||
|
// 288 Sa Po Kong
|
||||||
|
// Location: &query.GeoLocation{22.386886035837605, 113.92123399401174},
|
||||||
|
})
|
||||||
|
mesg, err = qo.Message()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected Error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range qo.GetTableData() {
|
||||||
|
for _, cell := range row {
|
||||||
|
fmt.Printf("| %s |", cell.Name)
|
||||||
|
}
|
||||||
|
fmt.Print("\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package i18n
|
|
||||||
|
|
||||||
import (
|
|
||||||
query "github.com/tgckpg/golifehk/query"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Generics struct {
|
|
||||||
Name *map[string]string
|
|
||||||
query.Words
|
|
||||||
}
|
|
||||||
8
i18n/distance.go
Normal file
8
i18n/distance.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
func FormatDistance(langPack LangPack, meters float64) string {
|
||||||
|
if meters < 1000 {
|
||||||
|
return UNITS_METER.Text(langPack, meters)
|
||||||
|
}
|
||||||
|
return UNITS_KM.Text(langPack, meters/1000)
|
||||||
|
}
|
||||||
66
i18n/functions.go
Normal file
66
i18n/functions.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
langRes "github.com/tgckpg/golifehk/resources/langpacks"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LangPack map[string]string
|
||||||
|
|
||||||
|
var (
|
||||||
|
LangPacks = map[string]LangPack{}
|
||||||
|
defaultLang = "en"
|
||||||
|
langMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadKeys(lang string) (LangPack, error) {
|
||||||
|
langMu.RLock()
|
||||||
|
langPack, ok := LangPacks[lang]
|
||||||
|
langMu.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return langPack, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
langMu.Lock()
|
||||||
|
defer langMu.Unlock()
|
||||||
|
|
||||||
|
// check again after obtaining write lock
|
||||||
|
if langPack, ok := LangPacks[lang]; ok {
|
||||||
|
return langPack, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := langRes.FS.ReadFile(lang + "/messages.json")
|
||||||
|
if err == nil {
|
||||||
|
var parsed LangPack
|
||||||
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
LangPacks[lang] = parsed
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if lang == defaultLang {
|
||||||
|
return nil, fmt.Errorf("No language packs available: %s", lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid recursive locking mess; explicit fallback is cleaner
|
||||||
|
if fallback, ok := LangPacks[defaultLang]; ok {
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = langRes.FS.ReadFile("langpacks/" + defaultLang + "/messages.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("No language packs available: %s", lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallback LangPack
|
||||||
|
if err := json.Unmarshal(data, &fallback); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
LangPacks[defaultLang] = fallback
|
||||||
|
return fallback, nil
|
||||||
|
}
|
||||||
15
i18n/messages.go
Normal file
15
i18n/messages.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
const (
|
||||||
|
DS_KMB_ETA_DEPARTED Key = "DS.KMB.ETA.DEPARTED"
|
||||||
|
DS_KMB_NEAREST_STOPS Key = "DS.KMB.NEAREST_STOPS"
|
||||||
|
DS_KMB_NO_NEAREST_STOPS Key = "DS.KMB.NO_NEAREST_STOPS"
|
||||||
|
DS_MTR_NEAREST_STOPS Key = "DS.MTR.NEAREST_STOPS"
|
||||||
|
DS_MTR_NO_NEAREST_STOPS Key = "DS.MTR.NO_NEAREST_STOPS"
|
||||||
|
DS_MTR_NO_SCHEDULES Key = "DS.MTR.NO_SCHEDULES"
|
||||||
|
NO_RESULTS Key = "NO_RESULTS"
|
||||||
|
UNITS_MINUTE Key = "UNITS.MINUTE"
|
||||||
|
UNITS_KM Key = "UNITS.KM"
|
||||||
|
UNITS_METER Key = "UNITS.METER"
|
||||||
|
UNITS_MILE Key = "UNITS.MILE"
|
||||||
|
)
|
||||||
31
i18n/race_test.go
Normal file
31
i18n/race_test.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadKeysRace(t *testing.T) {
|
||||||
|
// Reset shared global state so the test starts clean
|
||||||
|
LangPacks = map[string]LangPack{}
|
||||||
|
|
||||||
|
const goroutines = 200
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines)
|
||||||
|
|
||||||
|
start := make(chan struct{})
|
||||||
|
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
<-start
|
||||||
|
_, _ = LoadKeys("en")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release all goroutines at once to maximize overlap
|
||||||
|
close(start)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
26
i18n/types.go
Normal file
26
i18n/types.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package i18n
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
query "github.com/tgckpg/golifehk/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Generics struct {
|
||||||
|
Name *map[string]string
|
||||||
|
query.Words
|
||||||
|
}
|
||||||
|
|
||||||
|
type Key string
|
||||||
|
|
||||||
|
func (k Key) Text(langPack map[string]string, args ...any) string {
|
||||||
|
txt, ok := langPack[string(k)]
|
||||||
|
if !ok {
|
||||||
|
return string(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
return fmt.Sprintf(txt, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return txt
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MatchNearest(p IGeoLocation, entries *[]ISearchable, dist float64, limit int) (*QueryObject, error) {
|
func MatchNearest(p IGeoLocation, entries *[]ISearchable, dist float64, fallback_limit int) (*QueryObject, error) {
|
||||||
|
|
||||||
terms := []*QTerm{
|
terms := []*QTerm{
|
||||||
{
|
{
|
||||||
@@ -19,12 +19,22 @@ func MatchNearest(p IGeoLocation, entries *[]ISearchable, dist float64, limit in
|
|||||||
locs.SortByNearest(p)
|
locs.SortByNearest(p)
|
||||||
|
|
||||||
matches := []ISearchable{}
|
matches := []ISearchable{}
|
||||||
for i, item := range *locs {
|
for _, item := range *locs {
|
||||||
loc := item.(IGeoLocation)
|
loc := item.(IGeoLocation)
|
||||||
if i < limit && loc.Dist(p.Lat(), p.Lon()) <= dist {
|
if loc.Dist(p.Lat(), p.Lon()) <= dist {
|
||||||
matches = append(matches, item)
|
matches = append(matches, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(matches) == 0 {
|
||||||
|
for i, item := range *locs {
|
||||||
|
if i < fallback_limit {
|
||||||
|
matches = append(matches, item)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &QueryObject{Key: "", Results: &matches, SearchTerms: &terms}, nil
|
return &QueryObject{Key: "", Results: &matches, SearchTerms: &terms}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
6
resources/langpacks/embed.go
Normal file
6
resources/langpacks/embed.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package resources
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed */*.json
|
||||||
|
var FS embed.FS
|
||||||
60
resources/po/zh-Hant/messages.po
Normal file
60
resources/po/zh-Hant/messages.po
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#. extracted from ./resources/src/messages.json
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-03-10 15:02+0800\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Translate Toolkit 3.19.3\n"
|
||||||
|
|
||||||
|
#: .DS.KMB.ETA.DEPARTED
|
||||||
|
msgid "%s (Leaved?)"
|
||||||
|
msgstr "%s (走左了?)"
|
||||||
|
|
||||||
|
#: .DS.KMB.NEAREST_STOPS
|
||||||
|
msgid "KMB: Showing nearest bus stops under %s"
|
||||||
|
msgstr "九巴:%s 範圍內車站"
|
||||||
|
|
||||||
|
#: .DS.KMB.NO_NEAREST_STOPS
|
||||||
|
msgid ""
|
||||||
|
"KMB: Unable to find stations under %s. Listing nearest %d stations instead:"
|
||||||
|
msgstr "%s 範圍內找不到九巴站,最近 %d 個站為:"
|
||||||
|
|
||||||
|
#: .DS.MTR.NEAREST_STOPS
|
||||||
|
msgid "MTR: Showing nearest bus stops under %s"
|
||||||
|
msgstr "%s 範圍內港鐵巴士車站"
|
||||||
|
|
||||||
|
#: .DS.MTR.NO_NEAREST_STOPS
|
||||||
|
msgid ""
|
||||||
|
"MTR Bus: Unable to find stations under %s. Listing nearest %d stations "
|
||||||
|
"instead:"
|
||||||
|
msgstr "%s 範圍內找不到港鐵巴士站,最近 %d 個站為:"
|
||||||
|
|
||||||
|
#: .DS.MTR.NO_SCHEDULES
|
||||||
|
msgid "Schedules are empty...perhaps Out of Service Time?"
|
||||||
|
msgstr "沒有行程(收左工了?)"
|
||||||
|
|
||||||
|
#: .NO_RESULTS
|
||||||
|
msgid "No Results"
|
||||||
|
msgstr "找不到結果"
|
||||||
|
|
||||||
|
#: .UNITS.KM
|
||||||
|
msgid "%.1f km"
|
||||||
|
msgstr "%.1f 公里"
|
||||||
|
|
||||||
|
#: .UNITS.METER
|
||||||
|
msgid "%.0f m"
|
||||||
|
msgstr "%.0f 米"
|
||||||
|
|
||||||
|
#: .UNITS.MILE
|
||||||
|
msgid "%.1f mi"
|
||||||
|
msgstr "%.1f 英哩"
|
||||||
|
|
||||||
|
#: .UNITS.MINUTE
|
||||||
|
msgid "%.0f min"
|
||||||
|
msgstr "%.0f 分"
|
||||||
61
resources/pot/messages.pot
Normal file
61
resources/pot/messages.pot
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#. extracted from ./resources/src/messages.json
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-03-10 15:02+0800\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"X-Generator: Translate Toolkit 3.19.3\n"
|
||||||
|
|
||||||
|
#: .DS.KMB.ETA.DEPARTED
|
||||||
|
msgid "%s (Leaved?)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .DS.KMB.NEAREST_STOPS
|
||||||
|
msgid "KMB: Showing nearest bus stops under %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .DS.KMB.NO_NEAREST_STOPS
|
||||||
|
msgid ""
|
||||||
|
"KMB: Unable to find stations under %s. Listing nearest %d stations instead:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .DS.MTR.NEAREST_STOPS
|
||||||
|
msgid "MTR: Showing nearest bus stops under %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .DS.MTR.NO_NEAREST_STOPS
|
||||||
|
msgid ""
|
||||||
|
"MTR Bus: Unable to find stations under %s. Listing nearest %d stations "
|
||||||
|
"instead:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .DS.MTR.NO_SCHEDULES
|
||||||
|
msgid "Schedules are empty...perhaps Out of Service Time?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .NO_RESULTS
|
||||||
|
msgid "No Results"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .UNITS.KM
|
||||||
|
msgid "%.1f km"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .UNITS.METER
|
||||||
|
msgid "%.0f m"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .UNITS.MILE
|
||||||
|
msgid "%.1f mi"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: .UNITS.MINUTE
|
||||||
|
msgid "%.0f min"
|
||||||
|
msgstr ""
|
||||||
Reference in New Issue
Block a user