gettext-universal 1.0.13 → 1.0.15

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.
Files changed (46) hide show
  1. package/build/bin/gettext-universal.d.ts +3 -0
  2. package/build/bin/gettext-universal.d.ts.map +1 -0
  3. package/build/bin/gettext-universal.js +38 -0
  4. package/build/bin/po2js.d.ts +3 -0
  5. package/build/bin/po2js.d.ts.map +1 -0
  6. package/build/bin/po2js.js +15 -0
  7. package/build/index.d.ts +2 -0
  8. package/build/index.d.ts.map +1 -0
  9. package/build/index.js +2 -0
  10. package/build/src/config.d.ts +23 -0
  11. package/build/src/config.d.ts.map +1 -0
  12. package/build/src/config.js +39 -0
  13. package/build/src/events.d.ts +4 -0
  14. package/build/src/events.d.ts.map +1 -0
  15. package/build/src/events.js +4 -0
  16. package/build/src/po2js.d.ts +17 -0
  17. package/build/src/po2js.d.ts.map +1 -0
  18. package/build/src/po2js.js +42 -0
  19. package/build/src/scanner.d.ts +53 -0
  20. package/build/src/scanner.d.ts.map +1 -0
  21. package/build/src/scanner.js +135 -0
  22. package/build/src/translate-context.d.ts +3 -0
  23. package/build/src/translate-context.d.ts.map +1 -0
  24. package/build/src/translate-context.js +3 -0
  25. package/build/src/translate.d.ts +16 -0
  26. package/build/src/translate.d.ts.map +1 -0
  27. package/build/src/translate.js +77 -0
  28. package/build/src/use-translate-expo.d.ts +3 -0
  29. package/build/src/use-translate-expo.d.ts.map +1 -0
  30. package/build/src/use-translate-expo.js +26 -0
  31. package/build/src/with-translate.d.ts +6 -0
  32. package/build/src/with-translate.d.ts.map +1 -0
  33. package/build/src/with-translate.js +33 -0
  34. package/package.json +17 -4
  35. package/.github/dependabot.yml +0 -9
  36. package/bin/po2js.js +0 -20
  37. package/peak_flow.yml +0 -4
  38. package/spec/gettext-universal.spec.js +0 -41
  39. package/spec/support/jasmine.js +0 -14
  40. package/src/config.js +0 -39
  41. package/src/events.js +0 -5
  42. package/src/po2js.js +0 -43
  43. package/src/scanner.js +0 -147
  44. package/src/translate.js +0 -77
  45. package/src/use-translate-expo.js +0 -79
  46. package/test.po +0 -9
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=gettext-universal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gettext-universal.d.ts","sourceRoot":"","sources":["../../bin/gettext-universal.js"],"names":[],"mappings":""}
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import Scanner from "../src/scanner.js";
3
+ const processArgs = process.argv.slice(2);
4
+ const extensions = [];
5
+ const files = [];
6
+ const ignores = [];
7
+ let directory, output;
8
+ for (let i = 0; i < processArgs.length; i++) {
9
+ const arg = processArgs[i];
10
+ if (arg == "--directory") {
11
+ directory = processArgs[++i];
12
+ }
13
+ else if (arg == "--extension") {
14
+ extensions.push(processArgs[++i]);
15
+ }
16
+ else if (arg == "--files") {
17
+ while (i < processArgs.length - 1) {
18
+ const file = processArgs[++i];
19
+ if (!file)
20
+ throw new Error("No file found?");
21
+ files.push(file);
22
+ }
23
+ }
24
+ else if (arg == "--ignore") {
25
+ ignores.push(processArgs[++i]);
26
+ }
27
+ else if (arg == "--output") {
28
+ output = processArgs[++i];
29
+ }
30
+ else {
31
+ throw new Error(`Unknown argument: ${arg}`);
32
+ }
33
+ }
34
+ if (extensions.length == 0) {
35
+ extensions.push(".js", ".cjs", ".mjs", ".jsx", ".tsx");
36
+ }
37
+ const scanner = new Scanner({ directory, extensions, files, ignores, output });
38
+ await scanner.scan();
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=po2js.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"po2js.d.ts","sourceRoot":"","sources":["../../bin/po2js.js"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import Po2Js from "../src/po2js.js";
3
+ const processArgs = process.argv.slice(2);
4
+ let directory;
5
+ for (let i = 0; i < processArgs.length; i++) {
6
+ const arg = processArgs[i];
7
+ if (arg == "--directory") {
8
+ directory = processArgs[++i];
9
+ }
10
+ else {
11
+ throw new Error(`Unknown argument: ${arg}`);
12
+ }
13
+ }
14
+ const po2Js = new Po2Js({ directory });
15
+ await po2Js.run();
@@ -0,0 +1,2 @@
1
+ export const stub: "Hello world";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.js"],"names":[],"mappings":"AAAA,mBAAa,aAAa,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,2 @@
1
+ const stub = "Hello world";
2
+ export { stub };
@@ -0,0 +1,23 @@
1
+ export default config;
2
+ declare const config: Config;
3
+ declare class Config {
4
+ /** @type {Record<string, Record<string, any>>} */
5
+ locales: Record<string, Record<string, any>>;
6
+ loadTranslationsFromRequireContext(requireContext: any): void;
7
+ getFallbacks(): string[];
8
+ getLocale(): string;
9
+ getLocales(): Record<string, Record<string, any>>;
10
+ /**
11
+ * @param {string[]} fallbacks
12
+ * @returns {void}
13
+ */
14
+ setFallbacks(fallbacks: string[]): void;
15
+ fallbacks: string[];
16
+ /**
17
+ * @param {string} locale
18
+ * @returns {void}
19
+ */
20
+ setLocale(locale: string): void;
21
+ locale: string;
22
+ }
23
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.js"],"names":[],"mappings":";AA+CA,6BAA2B;AA3C3B;IAEI,kDAAkD;IAClD,SADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAC7B;IAGnB,8DAaC;IAED,yBAAwC;IACxC,oBAAkC;IAClC,kDAAoC;IAEpC;;;OAGG;IACH,wBAHW,MAAM,EAAE,GACN,IAAI,CAIhB;IADC,oBAA0B;IAG5B;;;OAGG;IACH,kBAHW,MAAM,GACJ,IAAI,CAKhB;IAFC,eAAoB;CAGvB"}
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+ import events from "./events.js";
3
+ class Config {
4
+ constructor() {
5
+ /** @type {Record<string, Record<string, any>>} */
6
+ this.locales = {};
7
+ }
8
+ loadTranslationsFromRequireContext(requireContext) {
9
+ for (const localeFile of requireContext.keys()) {
10
+ const match = localeFile.match(/([a-z]{2}).js$/);
11
+ if (!match) {
12
+ throw new Error(`Couldn't detect locale from file: ${localeFile}`);
13
+ }
14
+ const locale = match[1];
15
+ const translations = requireContext(localeFile).default;
16
+ this.locales[locale] = translations;
17
+ }
18
+ }
19
+ getFallbacks() { return this.fallbacks; }
20
+ getLocale() { return this.locale; }
21
+ getLocales() { return this.locales; }
22
+ /**
23
+ * @param {string[]} fallbacks
24
+ * @returns {void}
25
+ */
26
+ setFallbacks(fallbacks) {
27
+ this.fallbacks = fallbacks;
28
+ }
29
+ /**
30
+ * @param {string} locale
31
+ * @returns {void}
32
+ */
33
+ setLocale(locale) {
34
+ this.locale = locale;
35
+ events.emit("onLocaleChange", { locale });
36
+ }
37
+ }
38
+ const config = new Config();
39
+ export default config;
@@ -0,0 +1,4 @@
1
+ export default eventEmitter;
2
+ declare const eventEmitter: EventEmitter<any>;
3
+ import EventEmitter from "events";
4
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/events.js"],"names":[],"mappings":";AAIA,8CAAuC;yBAFd,QAAQ"}
@@ -0,0 +1,4 @@
1
+ // @ts-check
2
+ import EventEmitter from "events";
3
+ const eventEmitter = new EventEmitter();
4
+ export default eventEmitter;
@@ -0,0 +1,17 @@
1
+ export default class Po2Js {
2
+ /**
3
+ * @param {object} args
4
+ * @param {string} args.directory
5
+ */
6
+ constructor({ directory }: {
7
+ directory: string;
8
+ });
9
+ directory: string;
10
+ run(): Promise<void>;
11
+ /**
12
+ * @param {string} file
13
+ * @param {string} ext
14
+ */
15
+ readFile(file: string, ext: string): Promise<void>;
16
+ }
17
+ //# sourceMappingURL=po2js.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"po2js.d.ts","sourceRoot":"","sources":["../../src/po2js.js"],"names":[],"mappings":"AAKA;IACE;;;OAGG;IACH,2BAFG;QAAqB,SAAS,EAAtB,MAAM;KAChB,EAGA;IADC,kBAA0B;IAG5B,qBAUC;IAED;;;OAGG;IACH,eAHW,MAAM,OACN,MAAM,iBAuBhB;CACF"}
@@ -0,0 +1,42 @@
1
+ // @ts-check
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
4
+ export default class Po2Js {
5
+ /**
6
+ * @param {object} args
7
+ * @param {string} args.directory
8
+ */
9
+ constructor({ directory }) {
10
+ this.directory = directory;
11
+ }
12
+ async run() {
13
+ const files = await fs.readdir(this.directory);
14
+ for (const file of files) {
15
+ const ext = path.extname(file).toLowerCase();
16
+ if (ext == ".po") {
17
+ await this.readFile(file, ext);
18
+ }
19
+ }
20
+ }
21
+ /**
22
+ * @param {string} file
23
+ * @param {string} ext
24
+ */
25
+ async readFile(file, ext) {
26
+ const fullPath = `${this.directory}/${file}`;
27
+ const baseName = path.basename(file, ext);
28
+ const jsFilePath = `${this.directory}/${baseName}.js`;
29
+ const fileContentBuffer = await fs.readFile(fullPath);
30
+ const fileContent = fileContentBuffer.toString();
31
+ const matches = fileContent.matchAll(/#: (.+?)\nmsgid \"(.+?)\"\nmsgstr \"(.+?)\"\n(\n|$)/g);
32
+ const translations = {};
33
+ for (const match of matches) {
34
+ const msgId = match[2];
35
+ const msgStr = match[3];
36
+ translations[msgId] = msgStr;
37
+ }
38
+ const jsCode = `export default ${JSON.stringify(translations, null, 2)}\n`;
39
+ await fs.writeFile(jsFilePath, jsCode);
40
+ console.log(`Wrote ${jsFilePath}`);
41
+ }
42
+ }
@@ -0,0 +1,53 @@
1
+ export default class Scanner {
2
+ /**
3
+ * @param {object} args
4
+ * @param {string} args.directory
5
+ * @param {string[]} args.extensions
6
+ * @param {string[]} args.files
7
+ * @param {string[]} args.ignores
8
+ * @param {string} args.output
9
+ */
10
+ constructor({ directory, extensions, files, ignores, output, ...restArgs }: {
11
+ directory: string;
12
+ extensions: string[];
13
+ files: string[];
14
+ ignores: string[];
15
+ output: string;
16
+ });
17
+ directory: string;
18
+ extensions: string[];
19
+ files: string[];
20
+ ignores: string[];
21
+ output: string;
22
+ /** @type {Array<{fullPath: string, localPath: string}>} */
23
+ scannedFiles: Array<{
24
+ fullPath: string;
25
+ localPath: string;
26
+ }>;
27
+ /** @type {Record<string, {files: Array<{localPath: string, lineNumber: number}>}>} */
28
+ translations: Record<string, {
29
+ files: Array<{
30
+ localPath: string;
31
+ lineNumber: number;
32
+ }>;
33
+ }>;
34
+ scan(): Promise<void>;
35
+ scanGivenFiles(): Promise<void>;
36
+ scanFiles(): Promise<void>;
37
+ scanFile({ localPath, fullPath }: {
38
+ localPath: any;
39
+ fullPath: any;
40
+ }): Promise<void>;
41
+ scanDir(pathToScan: any, pathParts: any): Promise<void>;
42
+ /**
43
+ * @param {object} args
44
+ * @param {string} args.fullPath
45
+ * @param {string} args.localPath
46
+ */
47
+ addScannedFile({ fullPath, localPath }: {
48
+ fullPath: string;
49
+ localPath: string;
50
+ }): void;
51
+ writeOutput(): Promise<void>;
52
+ }
53
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/scanner.js"],"names":[],"mappings":"AAGA;IACE;;;;;;;OAOG;IACH,4EANG;QAAqB,SAAS,EAAtB,MAAM;QACS,UAAU,EAAzB,MAAM,EAAE;QACO,KAAK,EAApB,MAAM,EAAE;QACO,OAAO,EAAtB,MAAM,EAAE;QACK,MAAM,EAAnB,MAAM;KAChB,EAeA;IAXC,kBAA0B;IAC1B,qBAA4B;IAC5B,gBAAkB;IAClB,kBAAsB;IACtB,eAAoB;IAEpB,2DAA2D;IAC3D,cADW,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,CAAC,CACjC;IAEtB,sFAAsF;IACtF,cADW,MAAM,CAAC,MAAM,EAAE;QAAC,KAAK,EAAE,KAAK,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAC,CAAC,CAAA;KAAC,CAAC,CAC5D;IAGxB,sBAgBC;IAED,gCAOC;IAED,2BAQC;IAED;;;sBAyBC;IAED,wDA4BC;IAED;;;;OAIG;IACH,wCAHG;QAAqB,QAAQ,EAArB,MAAM;QACO,SAAS,EAAtB,MAAM;KAChB,QAMA;IAED,6BA4BC;CACF"}
@@ -0,0 +1,135 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ export default class Scanner {
4
+ /**
5
+ * @param {object} args
6
+ * @param {string} args.directory
7
+ * @param {string[]} args.extensions
8
+ * @param {string[]} args.files
9
+ * @param {string[]} args.ignores
10
+ * @param {string} args.output
11
+ */
12
+ constructor({ directory, extensions, files, ignores, output, ...restArgs }) {
13
+ if (Object.keys(restArgs).length > 0)
14
+ throw new Error(`Unknown arrguments: ${Object.keys(restArgs).join(", ")}`);
15
+ this.directory = directory;
16
+ this.extensions = extensions;
17
+ this.files = files;
18
+ this.ignores = ignores;
19
+ this.output = output;
20
+ /** @type {Array<{fullPath: string, localPath: string}>} */
21
+ this.scannedFiles = [];
22
+ /** @type {Record<string, {files: Array<{localPath: string, lineNumber: number}>}>} */
23
+ this.translations = {};
24
+ }
25
+ async scan() {
26
+ if (this.directory) {
27
+ await this.scanDir(this.directory, []);
28
+ }
29
+ else if (this.files.length > 0) {
30
+ await this.scanGivenFiles();
31
+ }
32
+ else {
33
+ throw new Error("No directory or files given to scan");
34
+ }
35
+ await this.scanFiles();
36
+ if (this.output) {
37
+ await this.writeOutput();
38
+ }
39
+ else {
40
+ console.log({ translations: this.translations });
41
+ }
42
+ }
43
+ async scanGivenFiles() {
44
+ for (const file of this.files) {
45
+ this.scannedFiles.push({
46
+ fullPath: file,
47
+ localPath: file,
48
+ });
49
+ }
50
+ }
51
+ async scanFiles() {
52
+ const promises = [];
53
+ for (const fileData of this.scannedFiles) {
54
+ promises.push(this.scanFile(fileData));
55
+ }
56
+ await Promise.all(promises);
57
+ }
58
+ async scanFile({ localPath, fullPath }) {
59
+ if (!fullPath)
60
+ throw new Error(`Invalid fullPath given: ${fullPath}`);
61
+ const contentBuffer = await fs.readFile(fullPath);
62
+ const content = await contentBuffer.toString();
63
+ const lines = content.split(/(\r\n|\n)/);
64
+ for (let lineNumber = 1; lineNumber < lines.length; lineNumber++) {
65
+ const line = lines[lineNumber - 1];
66
+ const match = line.match(/_\(\s*("(.+?)"|'(.+?)')\s*(,|\))/);
67
+ if (match) {
68
+ const translationKey = match[2] || match[3];
69
+ if (!translationKey)
70
+ throw new Error(`Empty translation key from match: ${JSON.stringify(match)}`);
71
+ if (!(translationKey in this.translations)) {
72
+ this.translations[translationKey] = {
73
+ files: []
74
+ };
75
+ }
76
+ this.translations[translationKey].files.push({ localPath, lineNumber });
77
+ }
78
+ }
79
+ }
80
+ async scanDir(pathToScan, pathParts) {
81
+ const files = await fs.readdir(pathToScan);
82
+ const localPath = pathParts.join("/");
83
+ if (this.ignores.includes(localPath)) {
84
+ // console.log(`Ignoreing ${localPath}`)
85
+ return;
86
+ }
87
+ // console.log({files, path: pathParts.join("/")})
88
+ for (const file of files) {
89
+ const fullPath = `${pathToScan}/${file}`;
90
+ const stat = await fs.lstat(fullPath);
91
+ if (stat.isDirectory()) {
92
+ const newPathParts = pathParts.concat([file]);
93
+ this.scanDir(fullPath, newPathParts);
94
+ }
95
+ else {
96
+ const ext = path.extname(file).toLowerCase();
97
+ if (this.extensions.includes(ext)) {
98
+ this.addScannedFile({ fullPath, localPath: `${localPath}/${file}` });
99
+ }
100
+ }
101
+ }
102
+ }
103
+ /**
104
+ * @param {object} args
105
+ * @param {string} args.fullPath
106
+ * @param {string} args.localPath
107
+ */
108
+ addScannedFile({ fullPath, localPath }) {
109
+ this.scannedFiles.push({
110
+ fullPath,
111
+ localPath,
112
+ });
113
+ }
114
+ async writeOutput() {
115
+ const fp = await fs.open(this.output, "w");
116
+ let translationsCount = 0;
117
+ await fp.write("msgid \"\"\n");
118
+ await fp.write("msgstr \"\"\n");
119
+ await fp.write("\"Content-Type: text/plain; charset=UTF-8\\n\"\n");
120
+ await fp.write("\n"); // Empty line after header
121
+ for (const translationKey in this.translations) {
122
+ const translation = this.translations[translationKey];
123
+ if (translationsCount >= 1)
124
+ await fp.write("\n");
125
+ for (const file of translation.files) {
126
+ const { localPath, lineNumber } = file;
127
+ await fp.write(`#: ${localPath}:${lineNumber}\n`);
128
+ }
129
+ await fp.write(`msgid \"${translationKey}\"\n`);
130
+ await fp.write("msgstr \"\"\n");
131
+ translationsCount++;
132
+ }
133
+ await fp.close();
134
+ }
135
+ }
@@ -0,0 +1,3 @@
1
+ export default TranslateContext;
2
+ declare const TranslateContext: any;
3
+ //# sourceMappingURL=translate-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate-context.d.ts","sourceRoot":"","sources":["../../src/translate-context.js"],"names":[],"mappings":";AAEA,oCAAwC"}
@@ -0,0 +1,3 @@
1
+ import { createContext } from "react";
2
+ const TranslateContext = createContext();
3
+ export default TranslateContext;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @typedef {object} TranslateArgs
3
+ * @property {string} [defaultValue]
4
+ * @property {string[]} [locales]
5
+ */
6
+ /**
7
+ * @param {string} msgId
8
+ * @param {Record<string, any>} [variables]
9
+ * @param {TranslateArgs | string[]} [argsCandidate] preferredLocales or arguments
10
+ */
11
+ export default function translate(msgId: string, variables?: Record<string, any>, argsCandidate?: TranslateArgs | string[]): any;
12
+ export type TranslateArgs = {
13
+ defaultValue?: string;
14
+ locales?: string[];
15
+ };
16
+ //# sourceMappingURL=translate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/translate.js"],"names":[],"mappings":"AAIA;;;;GAIG;AAEH;;;;GAIG;AACH,yCAJW,MAAM,cACN,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,kBACnB,aAAa,GAAG,MAAM,EAAE,OA8ElC;;mBArFa,MAAM;cACN,MAAM,EAAE"}
@@ -0,0 +1,77 @@
1
+ // @ts-check
2
+ import config from "./config.js";
3
+ /**
4
+ * @typedef {object} TranslateArgs
5
+ * @property {string} [defaultValue]
6
+ * @property {string[]} [locales]
7
+ */
8
+ /**
9
+ * @param {string} msgId
10
+ * @param {Record<string, any>} [variables]
11
+ * @param {TranslateArgs | string[]} [argsCandidate] preferredLocales or arguments
12
+ */
13
+ export default function translate(msgId, variables, argsCandidate) {
14
+ let args;
15
+ let preferredLocales;
16
+ if (Array.isArray(argsCandidate)) {
17
+ preferredLocales = argsCandidate;
18
+ }
19
+ else if (typeof args == "object") {
20
+ args = argsCandidate;
21
+ if (args.locales) {
22
+ preferredLocales = args.locales;
23
+ }
24
+ }
25
+ if (!preferredLocales) {
26
+ if (config.getLocale()) {
27
+ preferredLocales = [config.getLocale()];
28
+ }
29
+ else {
30
+ console.error("No 'preferredLocales' was given and a locale wasn't set in the configuration either");
31
+ }
32
+ }
33
+ let translation;
34
+ if (preferredLocales) {
35
+ for (const preferredLocale of preferredLocales) {
36
+ const localeTranslations = config.getLocales()[preferredLocale];
37
+ if (!localeTranslations)
38
+ continue;
39
+ const localeTranslation = localeTranslations[msgId];
40
+ if (localeTranslation) {
41
+ translation = localeTranslation;
42
+ break;
43
+ }
44
+ }
45
+ }
46
+ if (!translation) {
47
+ const fallbacks = config.getFallbacks();
48
+ if (fallbacks) {
49
+ for (const fallback of config.getFallbacks()) {
50
+ const localeTranslations = config.getLocales()[fallback];
51
+ if (!localeTranslations)
52
+ continue;
53
+ const localeTranslation = localeTranslations[msgId];
54
+ if (localeTranslation) {
55
+ translation = localeTranslation;
56
+ break;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ if (!translation) {
62
+ if (args?.defaultValue) {
63
+ translation = args.defaultValue;
64
+ }
65
+ else {
66
+ translation = msgId;
67
+ }
68
+ }
69
+ if (variables) {
70
+ for (const key in variables) {
71
+ const value = variables[key];
72
+ const replaceKey = `%{${key}}`;
73
+ translation = translation.replaceAll(replaceKey, value);
74
+ }
75
+ }
76
+ return translation;
77
+ }
@@ -0,0 +1,3 @@
1
+ export default useTranslateExpo;
2
+ declare function useTranslateExpo(): any;
3
+ //# sourceMappingURL=use-translate-expo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-translate-expo.d.ts","sourceRoot":"","sources":["../../src/use-translate-expo.js"],"names":[],"mappings":";AAKA,yCA2BC"}
@@ -0,0 +1,26 @@
1
+ import translate from "./translate.js";
2
+ import TranslateContext from "./translate-context.js";
3
+ import { useCallback, useContext, useMemo } from "react";
4
+ import { useLocales } from "expo-localization";
5
+ const useTranslateExpo = () => {
6
+ const localeContext = useContext(TranslateContext);
7
+ const locales = useLocales();
8
+ const preferredLocales = useMemo(() => {
9
+ let preferredLocales = [];
10
+ if (localeContext?.locale) {
11
+ preferredLocales.push(localeContext.locale);
12
+ }
13
+ if (Array.isArray(locales)) {
14
+ for (const localeData of locales) {
15
+ preferredLocales.push(localeData.languageCode);
16
+ }
17
+ }
18
+ return preferredLocales;
19
+ }, [localeContext?.locale, locales]);
20
+ const currentTranslation = useCallback((msgId, variables, args = {}) => {
21
+ args.locales ||= preferredLocales;
22
+ return translate(msgId, variables, args);
23
+ }, [preferredLocales]);
24
+ return currentTranslation;
25
+ };
26
+ export default useTranslateExpo;
@@ -0,0 +1,6 @@
1
+ export default WithTranslate;
2
+ declare function WithTranslate({ children, ...restProps }: {
3
+ [x: string]: any;
4
+ children: any;
5
+ }): any;
6
+ //# sourceMappingURL=with-translate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with-translate.d.ts","sourceRoot":"","sources":["../../src/with-translate.jsx"],"names":[],"mappings":";AAOA;;;QAsCC"}
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import config from "./config.js";
3
+ import events from "./events.js";
4
+ import { useCallback, useMemo, useState } from "react";
5
+ import TranslateContext from "./translate-context.js";
6
+ import useEventEmitter from "@kaspernj/api-maker/build/use-event-emitter.js";
7
+ import { useLocales } from "expo-localization";
8
+ const WithTranslate = ({ children, ...restProps }) => {
9
+ const locales = useLocales();
10
+ const [locale, setLocale] = useState(config.getLocale());
11
+ const actualLocales = useMemo(() => {
12
+ const actualLocales = [];
13
+ if (locale) {
14
+ actualLocales.push(locale);
15
+ }
16
+ for (const locale of locales) {
17
+ actualLocales.push(locale.languageCode);
18
+ }
19
+ return actualLocales;
20
+ }, [locale, locales]);
21
+ const contextData = useMemo(() => ({ locale, locales: actualLocales }), [actualLocales]);
22
+ const onChangeLocale = useCallback(({ locale }) => {
23
+ setLocale(locale);
24
+ }, []);
25
+ // @ts-expect-error
26
+ useEventEmitter(events, "onLocaleChange", onChangeLocale);
27
+ const restPropsKeys = Object.keys(restProps);
28
+ if (restPropsKeys.length > 0) {
29
+ throw new Error(`Unknown props given: ${restPropsKeys.join(", ")}`);
30
+ }
31
+ return (_jsx(TranslateContext.Provider, { value: contextData, children: children }));
32
+ };
33
+ export default WithTranslate;
package/package.json CHANGED
@@ -4,12 +4,19 @@
4
4
  },
5
5
  "name": "gettext-universal",
6
6
  "type": "module",
7
- "version": "1.0.13",
8
- "main": "index.js",
7
+ "version": "1.0.15",
8
+ "main": "build/index.js",
9
+ "types": "build/index.d.ts",
10
+ "files": ["build/**"],
9
11
  "scripts": {
12
+ "build": "rm -rf build && tsc && npm run chmod-executables",
13
+ "chmod-executables": "chmod +x build/bin/gettext-universal.js && chmod +x build/bin/po2js.js",
10
14
  "gettext-universal": "node bin/gettext-universal.js",
11
15
  "po2js": "node bin/po2js.js",
12
- "test": "jasmine"
16
+ "prepublishOnly": "npm run build",
17
+ "test": "jasmine",
18
+ "typecheck": "tsc --noEmit",
19
+ "watch": "tsc -w"
13
20
  },
14
21
  "repository": {
15
22
  "type": "git",
@@ -31,6 +38,12 @@
31
38
  "diggerize": "^1.0.5"
32
39
  },
33
40
  "devDependencies": {
34
- "jasmine": "^5.7.1"
41
+ "jasmine": "^5.7.1",
42
+ "typescript": "^5.9.3"
43
+ },
44
+ "peerDependencies": {
45
+ "@kaspernj/api-maker": "*",
46
+ "expo-localization": "*",
47
+ "react": "*"
35
48
  }
36
49
  }
@@ -1,9 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: npm
4
- directory: "/"
5
- schedule:
6
- interval: weekly
7
- time: "01:00"
8
- timezone: Europe/Berlin
9
- open-pull-requests-limit: 99
package/bin/po2js.js DELETED
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import Po2Js from "../src/po2js.js"
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 po2Js = new Po2Js({directory})
19
-
20
- await po2Js.run()
package/peak_flow.yml DELETED
@@ -1,4 +0,0 @@
1
- before_script:
2
- - npm install
3
- script:
4
- - npm test
@@ -1,41 +0,0 @@
1
- import config from "../src/config.js"
2
- import translate from "../src/translate.js"
3
-
4
- describe("gettext-universal", () => {
5
- it("falls back to the given msgId", () => {
6
- config.setLocale("en")
7
- config.locales = {
8
- en: {}
9
- }
10
-
11
- const result = translate("Hello world")
12
-
13
- expect(result).toEqual("Hello world")
14
- })
15
-
16
- it("replaces placeholders with variables", () => {
17
- config.setLocale("en")
18
- config.locales = {
19
- "en": {
20
- "Hello name": "Hello %{name}"
21
- }
22
- }
23
-
24
- const result = translate("Hello name", {name: "Kasper"})
25
-
26
- expect(result).toEqual("Hello Kasper")
27
- })
28
-
29
- it("replaces placeholders with variables in defaults", () => {
30
- config.setLocale(null)
31
- config.locales = {
32
- "en": {
33
- "Hello world": "Hello world"
34
- }
35
- }
36
-
37
- const result = translate("Hello %{name}", {name: "Kasper"})
38
-
39
- expect(result).toEqual("Hello Kasper")
40
- })
41
- })
@@ -1,14 +0,0 @@
1
- export default {
2
- spec_dir: "spec",
3
- spec_files: [
4
- "**/*[sS]pec.js"
5
- ],
6
- helpers: [
7
- "helpers/**/*.js"
8
- ],
9
- env: {
10
- stopSpecOnExpectationFailure: false,
11
- random: true,
12
- forbidDuplicateNames: true
13
- }
14
- }
package/src/config.js DELETED
@@ -1,39 +0,0 @@
1
- import events from "./events.js"
2
-
3
- class Config {
4
- constructor() {
5
- this.locales = {}
6
- }
7
-
8
- loadTranslationsFromRequireContext(requireContext) {
9
- for (const localeFile of requireContext.keys()) {
10
- const match = localeFile.match(/([a-z]{2}).js$/)
11
-
12
- if (!match) {
13
- throw new Error(`Couldn't detect locale from file: ${localeFile}`)
14
- }
15
-
16
- const locale = match[1]
17
- const translations = requireContext(localeFile).default
18
-
19
- this.locales[locale] = translations
20
- }
21
- }
22
-
23
- getFallbacks = () => this.fallbacks
24
- getLocale = () => this.locale
25
- getLocales = () => this.locales
26
-
27
- setFallbacks(fallbacks) {
28
- this.fallbacks = fallbacks
29
- }
30
-
31
- setLocale(locale) {
32
- this.locale = locale
33
- events.emit("onLocaleChange", {locale})
34
- }
35
- }
36
-
37
- const config = new Config()
38
-
39
- export default config
package/src/events.js DELETED
@@ -1,5 +0,0 @@
1
- import EventEmitter from "events"
2
-
3
- const eventEmitter = new EventEmitter()
4
-
5
- export default eventEmitter
package/src/po2js.js DELETED
@@ -1,43 +0,0 @@
1
- import {promises as fs} from "fs"
2
- import path from "path"
3
-
4
- export default class Po2Js {
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}.js`
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.js DELETED
@@ -1,147 +0,0 @@
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
- await fp.write("msgid \"\"\n")
124
- await fp.write("msgstr \"\"\n")
125
- await fp.write("\"Content-Type: text/plain; charset=UTF-8\\n\"\n")
126
- await fp.write("\n") // Empty line after header
127
-
128
- for (const translationKey in this.translations) {
129
- const translation = this.translations[translationKey]
130
-
131
- if (translationsCount >= 1) await fp.write("\n")
132
-
133
- for (const file of translation.files) {
134
- const {localPath, lineNumber} = file
135
-
136
- await fp.write(`#: ${localPath}:${lineNumber}\n`)
137
- }
138
-
139
- await fp.write(`msgid \"${translationKey}\"\n`)
140
- await fp.write("msgstr \"\"\n")
141
-
142
- translationsCount++
143
- }
144
-
145
- await fp.close()
146
- }
147
- }
package/src/translate.js DELETED
@@ -1,77 +0,0 @@
1
- import config from "./config.js"
2
-
3
- const translate = (msgId, variables, args) => {
4
- let preferredLocales
5
-
6
- if (Array.isArray(args)) {
7
- preferredLocales = args
8
- args = null
9
- } else if (args?.locales) {
10
- preferredLocales = args.locales
11
- }
12
-
13
- if (!preferredLocales) {
14
- if (config.getLocale()) {
15
- preferredLocales = [config.getLocale()]
16
- } else {
17
- console.error("No 'preferredLocales' was given and a locale wasn't set in the configuration either")
18
- }
19
- }
20
-
21
- let translation
22
-
23
- if (preferredLocales) {
24
- for (const preferredLocale of preferredLocales) {
25
- const localeTranslations = config.getLocales()[preferredLocale]
26
-
27
- if (!localeTranslations) continue
28
-
29
- const localeTranslation = localeTranslations[msgId]
30
-
31
- if (localeTranslation) {
32
- translation = localeTranslation
33
- break
34
- }
35
- }
36
- }
37
-
38
- if (!translation) {
39
- const fallbacks = config.getFallbacks()
40
-
41
- if (fallbacks) {
42
- for (const fallback of config.getFallbacks()) {
43
- const localeTranslations = config.getLocales()[fallback]
44
-
45
- if (!localeTranslations) continue
46
-
47
- const localeTranslation = localeTranslations[msgId]
48
-
49
- if (localeTranslation) {
50
- translation = localeTranslation
51
- break
52
- }
53
- }
54
- }
55
- }
56
-
57
- if (!translation) {
58
- if (args?.defaultValue) {
59
- translation = args.defaultValue
60
- } else {
61
- translation = msgId
62
- }
63
- }
64
-
65
- if (variables) {
66
- for (const key in variables) {
67
- const value = variables[key]
68
- const replaceKey = `%{${key}}`
69
-
70
- translation = translation.replaceAll(replaceKey, value)
71
- }
72
- }
73
-
74
- return translation
75
- }
76
-
77
- export default translate
@@ -1,79 +0,0 @@
1
- import config from "./config.js"
2
- import events from "./events.js"
3
- import translate from "./translate.js"
4
- import {createContext, useCallback, useContext, useMemo, useState} from "react"
5
- import useEventEmitter from "@kaspernj/api-maker/build/use-event-emitter"
6
- import {useLocales} from "expo-localization"
7
-
8
- const TranslateContext = createContext()
9
-
10
- const WithTranslate = ({children, ...restProps}) => {
11
- const locales = useLocales()
12
- const [locale, setLocale] = useState(config.getLocale())
13
-
14
- const actualLocales = useMemo(() => {
15
- const actualLocales = []
16
-
17
- if (locale) {
18
- actualLocales.push(locale)
19
- }
20
-
21
- for (const locale of locales) {
22
- actualLocales.push(locale.languageCode)
23
- }
24
-
25
- return actualLocales
26
- }, [locale, locales])
27
-
28
- const contextData = useMemo(() => ({locale, locales: actualLocales}), [actualLocales])
29
-
30
- const onChangeLocale = useCallback(({locale}) => {
31
- setLocale(locale)
32
- }, [])
33
-
34
- useEventEmitter(events, "onLocaleChange", onChangeLocale)
35
-
36
- const restPropsKeys = Object.keys(restProps)
37
-
38
- if (restPropsKeys.length > 0) {
39
- throw new Error(`Unknown props given: ${restPropsKeys.join(", ")}`)
40
- }
41
-
42
- return (
43
- <TranslateContext.Provider value={contextData}>
44
- {children}
45
- </TranslateContext.Provider>
46
- )
47
- }
48
-
49
- const useTranslateExpo = () => {
50
- const localeContext = useContext(TranslateContext)
51
- const locales = useLocales()
52
-
53
- const preferredLocales = useMemo(() => {
54
- let preferredLocales = []
55
-
56
- if (localeContext?.locale) {
57
- preferredLocales.push(localeContext.locale)
58
- }
59
-
60
- if (Array.isArray(locales)) {
61
- for (const localeData of locales) {
62
- preferredLocales.push(localeData.languageCode)
63
- }
64
- }
65
-
66
- return preferredLocales
67
- }, [localeContext?.locale, locales])
68
-
69
- const currentTranslation = useCallback((msgId, variables, args = {}) => {
70
- args.locales ||= preferredLocales
71
-
72
- return translate(msgId, variables, args)
73
- }, [preferredLocales])
74
-
75
- return currentTranslation
76
- }
77
-
78
- export {WithTranslate}
79
- export default useTranslateExpo
package/test.po DELETED
@@ -1,9 +0,0 @@
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 ""