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.
@@ -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
+ }
@@ -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
package/test.po ADDED
@@ -0,0 +1,9 @@
1
+ #: app/(tabs)/index.tsx:51
2
+ #: app/sign-in.jsx:27
3
+ msgid "Sign in"
4
+ msgstr ""
5
+
6
+ #: app/(tabs)/index.tsx:61
7
+ #: app/sign-up.jsx:27
8
+ msgid "Sign up"
9
+ msgstr ""