gettext-universal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gettext-universal.mjs +41 -0
- package/bin/po2mjs.mjs +20 -0
- package/package.json +32 -0
- package/src/config.mjs +28 -0
- package/src/po2mjs.mjs +43 -0
- package/src/scanner.mjs +142 -0
- package/src/use-translate.mjs +48 -0
- package/test.po +9 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import Scanner from "../src/scanner.mjs"
|
|
4
|
+
|
|
5
|
+
const processArgs = process.argv.slice(2)
|
|
6
|
+
const extensions = []
|
|
7
|
+
const files = []
|
|
8
|
+
const ignores = []
|
|
9
|
+
let directory, output
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < processArgs.length; i++) {
|
|
12
|
+
const arg = processArgs[i]
|
|
13
|
+
|
|
14
|
+
if (arg == "--directory") {
|
|
15
|
+
directory = processArgs[++i]
|
|
16
|
+
} else if (arg == "--extension") {
|
|
17
|
+
extensions.push(processArgs[++i])
|
|
18
|
+
} else if (arg == "--files") {
|
|
19
|
+
while (i < processArgs.length - 1) {
|
|
20
|
+
const file = processArgs[++i]
|
|
21
|
+
|
|
22
|
+
if (!file) throw new Error("No file found?")
|
|
23
|
+
|
|
24
|
+
files.push(file)
|
|
25
|
+
}
|
|
26
|
+
} else if (arg == "--ignore") {
|
|
27
|
+
ignores.push(processArgs[++i])
|
|
28
|
+
} else if (arg == "--output") {
|
|
29
|
+
output = processArgs[++i]
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error(`Unknown argument: ${arg}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (extensions.length == 0) {
|
|
36
|
+
extensions.push(".js", ".cjs", ".mjs", ".jsx", ".tsx")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const scanner = new Scanner({directory, extensions, files, ignores, output})
|
|
40
|
+
|
|
41
|
+
await scanner.scan()
|
package/bin/po2mjs.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import Po2Mjs from "../src/po2mjs.mjs"
|
|
4
|
+
|
|
5
|
+
const processArgs = process.argv.slice(2)
|
|
6
|
+
let directory
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < processArgs.length; i++) {
|
|
9
|
+
const arg = processArgs[i]
|
|
10
|
+
|
|
11
|
+
if (arg == "--directory") {
|
|
12
|
+
directory = processArgs[++i]
|
|
13
|
+
} else {
|
|
14
|
+
throw new Error(`Unknown argument: ${arg}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const po2Mjs = new Po2Mjs({directory})
|
|
19
|
+
|
|
20
|
+
await po2Mjs.run()
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"gettext-universal": "bin/gettext-universal.mjs"
|
|
4
|
+
},
|
|
5
|
+
"name": "gettext-universal",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"gettext-universal": "node bin/gettext-universal.mjs",
|
|
10
|
+
"po2mjs": "node bin/po2mjs.mjs",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/kaspernj/gettext-universal.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"gettext",
|
|
19
|
+
"poedit",
|
|
20
|
+
"scan"
|
|
21
|
+
],
|
|
22
|
+
"author": "kasper@diestoeckels.de",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/kaspernj/gettext-universal/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/kaspernj/gettext-universal#readme",
|
|
28
|
+
"description": "",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"diggerize": "^1.0.5"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class Config {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.locales = {}
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
loadTranslationsFromRequireContext(requireContext) {
|
|
7
|
+
for (const localeFile of requireContext.keys()) {
|
|
8
|
+
const match = localeFile.match(/^\.\/([a-z]{2}).mjs$/)
|
|
9
|
+
|
|
10
|
+
if (!match) {
|
|
11
|
+
throw new Error(`Couldn't detect locale from file: ${localeFile}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const locale = match[1]
|
|
15
|
+
const translations = requireContext(localeFile).default
|
|
16
|
+
|
|
17
|
+
locales[locale] = translations
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setFallbacks(fallbacks) {
|
|
22
|
+
this.fallbacks = fallbacks
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = new Config()
|
|
27
|
+
|
|
28
|
+
export default config
|
package/src/po2mjs.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {promises as fs} from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
export default class Po2Mjs {
|
|
5
|
+
constructor({directory}) {
|
|
6
|
+
this.directory = directory
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async run() {
|
|
10
|
+
const files = await fs.readdir(this.directory)
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const ext = path.extname(file).toLowerCase()
|
|
14
|
+
|
|
15
|
+
if (ext == ".po") {
|
|
16
|
+
await this.readFile(file, ext)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async readFile(file, ext) {
|
|
22
|
+
const fullPath = `${this.directory}/${file}`
|
|
23
|
+
const baseName = path.basename(file, ext)
|
|
24
|
+
const jsFilePath = `${this.directory}/${baseName}.mjs`
|
|
25
|
+
const fileContentBuffer = await fs.readFile(fullPath)
|
|
26
|
+
const fileContent = fileContentBuffer.toString()
|
|
27
|
+
const matches = fileContent.matchAll(/#: (.+?)\nmsgid \"(.+?)\"\nmsgstr \"(.+?)\"\n(\n|$)/g)
|
|
28
|
+
const translations = {}
|
|
29
|
+
|
|
30
|
+
for (const match of matches) {
|
|
31
|
+
const msgId = match[2]
|
|
32
|
+
const msgStr = match[3]
|
|
33
|
+
|
|
34
|
+
translations[msgId] = msgStr
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const jsCode = `export default ${JSON.stringify(translations, null, 2)}\n`
|
|
38
|
+
|
|
39
|
+
await fs.writeFile(jsFilePath, jsCode)
|
|
40
|
+
|
|
41
|
+
console.log(`Wrote ${jsFilePath}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/scanner.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {promises as fs} from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
|
|
4
|
+
export default class Scanner {
|
|
5
|
+
constructor({directory, extensions, files, ignores, output, ...restArgs}) {
|
|
6
|
+
if (Object.keys(restArgs).length > 0) throw new Error(`Unknown arrguments: ${Object.keys(restArgs).join(", ")}`)
|
|
7
|
+
|
|
8
|
+
this.directory = directory
|
|
9
|
+
this.extensions = extensions
|
|
10
|
+
this.files = files
|
|
11
|
+
this.ignores = ignores
|
|
12
|
+
this.output = output
|
|
13
|
+
this.scannedFiles = []
|
|
14
|
+
this.translations = {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async scan() {
|
|
18
|
+
if (this.directory) {
|
|
19
|
+
await this.scanDir(this.directory, [])
|
|
20
|
+
} else if (this.files.length > 0) {
|
|
21
|
+
await this.scanGivenFiles()
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error("No directory or files given to scan")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await this.scanFiles()
|
|
27
|
+
|
|
28
|
+
if (this.output) {
|
|
29
|
+
await this.writeOutput()
|
|
30
|
+
} else {
|
|
31
|
+
console.log({translations: this.translations})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async scanGivenFiles() {
|
|
36
|
+
for (const file of this.files) {
|
|
37
|
+
this.scannedFiles.push({
|
|
38
|
+
fullPath: file,
|
|
39
|
+
localPath: file,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async scanFiles() {
|
|
45
|
+
const promises = []
|
|
46
|
+
|
|
47
|
+
for (const fileData of this.scannedFiles) {
|
|
48
|
+
promises.push(this.scanFile(fileData))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await Promise.all(promises)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async scanFile({localPath, fullPath}) {
|
|
55
|
+
if (!fullPath) throw new Error(`Invalid fullPath given: ${fullPath}`)
|
|
56
|
+
|
|
57
|
+
const contentBuffer = await fs.readFile(fullPath)
|
|
58
|
+
const content = await contentBuffer.toString()
|
|
59
|
+
const lines = content.split(/(\r\n|\n)/)
|
|
60
|
+
|
|
61
|
+
for (let lineNumber = 1; lineNumber < lines.length; lineNumber++) {
|
|
62
|
+
const line = lines[lineNumber - 1]
|
|
63
|
+
const match = line.match(/_\(\s*("(.+?)"|'(.+?)')\s*(,|\))/)
|
|
64
|
+
|
|
65
|
+
if (match) {
|
|
66
|
+
const translationKey = match[2] || match[3]
|
|
67
|
+
|
|
68
|
+
if (!translationKey) throw new Error("Empty translation key from match", {match})
|
|
69
|
+
|
|
70
|
+
if (!(translationKey in this.translations)) {
|
|
71
|
+
this.translations[translationKey] = {
|
|
72
|
+
files: []
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.translations[translationKey].files.push({localPath, lineNumber})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async scanDir(pathToScan, pathParts) {
|
|
82
|
+
const files = await fs.readdir(pathToScan)
|
|
83
|
+
const localPath = pathParts.join("/")
|
|
84
|
+
|
|
85
|
+
if (this.ignores.includes(localPath)) {
|
|
86
|
+
// console.log(`Ignoreing ${localPath}`)
|
|
87
|
+
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// console.log({files, path: pathParts.join("/")})
|
|
92
|
+
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const fullPath = `${pathToScan}/${file}`
|
|
95
|
+
const stat = await fs.lstat(fullPath)
|
|
96
|
+
|
|
97
|
+
if (stat.isDirectory()) {
|
|
98
|
+
const newPathParts = pathParts.concat([file])
|
|
99
|
+
|
|
100
|
+
this.scanDir(fullPath, newPathParts)
|
|
101
|
+
} else {
|
|
102
|
+
const ext = path.extname(file).toLowerCase()
|
|
103
|
+
|
|
104
|
+
if (this.extensions.includes(ext)) {
|
|
105
|
+
this.addScannedFile({fullPath, localPath: `${localPath}/${file}`})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
addScannedFile({fullPath, localPath}) {
|
|
112
|
+
this.scannedFiles.push({
|
|
113
|
+
fullPath,
|
|
114
|
+
localPath,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async writeOutput() {
|
|
119
|
+
const fp = await fs.open(this.output, "w")
|
|
120
|
+
|
|
121
|
+
let translationsCount = 0
|
|
122
|
+
|
|
123
|
+
for (const translationKey in this.translations) {
|
|
124
|
+
const translation = this.translations[translationKey]
|
|
125
|
+
|
|
126
|
+
if (translationsCount >= 1) await fp.write("\n")
|
|
127
|
+
|
|
128
|
+
for (const file of translation.files) {
|
|
129
|
+
const {localPath, lineNumber} = file
|
|
130
|
+
|
|
131
|
+
await fp.write(`#: ${localPath}:${lineNumber}\n`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await fp.write(`msgid \"${translationKey}\"\n`)
|
|
135
|
+
await fp.write("msgstr \"\"\n")
|
|
136
|
+
|
|
137
|
+
translationsCount++
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await fp.close()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {useCallback} from "react"
|
|
2
|
+
import {useLocales} from "expo-localization"
|
|
3
|
+
|
|
4
|
+
const translate = (msgId, preferredLocales) => {
|
|
5
|
+
let translation
|
|
6
|
+
|
|
7
|
+
for (preferredLocale of preferredLocales) {
|
|
8
|
+
const localeTranslations = locales[preferredLocale]
|
|
9
|
+
|
|
10
|
+
if (!localeTranslations) continue
|
|
11
|
+
|
|
12
|
+
const localeTranslation = localeTranslations[msgId]
|
|
13
|
+
|
|
14
|
+
if (localeTranslation) {
|
|
15
|
+
translation = localeTranslation
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!translation) {
|
|
21
|
+
for (const fallback of fallbacks) {
|
|
22
|
+
const localeTranslations = locales[fallback]
|
|
23
|
+
|
|
24
|
+
if (!localeTranslations) continue
|
|
25
|
+
|
|
26
|
+
const localeTranslation = localeTranslations[msgId]
|
|
27
|
+
|
|
28
|
+
if (localeTranslation) {
|
|
29
|
+
translation = localeTranslation
|
|
30
|
+
break
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!translation) translation = msgId
|
|
36
|
+
|
|
37
|
+
return translation
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const useTranslate = () => {
|
|
41
|
+
const locales = useLocales()
|
|
42
|
+
const preferredLocales = locales.map((localeData) => localeData.languageCode)
|
|
43
|
+
const currentTranslation = useCallback((msgId) => translate(msgId, preferredLocales), [preferredLocales])
|
|
44
|
+
|
|
45
|
+
return currentTranslation
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default useTranslate
|