@vocab/phrase 2.1.10 → 2.2.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/dist/README.md CHANGED
@@ -732,6 +732,17 @@ This flag accepts an array of glob patterns to ignore.
732
732
  vocab push --branch my-branch --ignore "**/dist/**" "**/another_ignored_directory/**"
733
733
  ```
734
734
 
735
+ #### Auto-Translation
736
+
737
+ By default, Phrase may not apply the project's automatic translation behaviour for new keys uploaded via API.
738
+
739
+ The `--auto-translate` flag instructs Phrase to automatically translate any missing keys using machine translation.. See [Phrase auto-translate API Documentation] for more information.
740
+
741
+ ```sh
742
+ vocab push --branch my-branch --auto-translate
743
+ ```
744
+
745
+ [Phrase auto-translate API Documentation]: https://developers.phrase.com/en/api/strings/uploads/upload-a-new-file#body-autotranslate
735
746
  [phrase]: https://developers.phrase.com/api/
736
747
 
737
748
  #### [Tags]
package/dist/index.cjs CHANGED
@@ -138,7 +138,7 @@ async function pullAllTranslations(branch) {
138
138
  }
139
139
  return translations;
140
140
  }
141
- async function pushTranslations(translationsByLanguage, { devLanguage, branch }) {
141
+ async function pushTranslations(translationsByLanguage, { autoTranslate, branch, devLanguage }) {
142
142
  const { csvFileStrings, keyIndex, commentIndex, tagColumn, messageIndex } = translationsToCsv(translationsByLanguage, devLanguage);
143
143
  let devLanguageUploadId = "";
144
144
  for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
@@ -148,6 +148,7 @@ async function pushTranslations(translationsByLanguage, { devLanguage, branch })
148
148
  formData.append("branch", branch);
149
149
  formData.append("update_translations", "true");
150
150
  formData.append("update_descriptions", "true");
151
+ if (autoTranslate) formData.append("autotranslate", "true");
151
152
  formData.append(`locale_mapping[${language}]`, messageIndex.toString());
152
153
  formData.append("format_options[key_index]", keyIndex.toString());
153
154
  formData.append("format_options[comment_index]", commentIndex.toString());
@@ -248,7 +249,7 @@ async function pull({ branch = "local-development", errorOnNoGlobalKeyTranslatio
248
249
  * Uploads translations to the Phrase API for each language.
249
250
  * A unique namespace is appended to each key using the file path the key came from.
250
251
  */
251
- async function push({ branch, deleteUnusedKeys: deleteUnusedKeys$1, ignore }, config) {
252
+ async function push({ autoTranslate, branch, deleteUnusedKeys: deleteUnusedKeys$1, ignore }, config) {
252
253
  if (ignore) trace(`ignoring files on paths: ${ignore.join(", ")}`);
253
254
  const allLanguageTranslations = await (0, _vocab_core.loadAllTranslations)({
254
255
  fallbacks: "none",
@@ -276,8 +277,9 @@ async function push({ branch, deleteUnusedKeys: deleteUnusedKeys$1, ignore }, co
276
277
  }
277
278
  }
278
279
  const { devLanguageUploadId } = await pushTranslations(phraseTranslations, {
279
- devLanguage: config.devLanguage,
280
- branch
280
+ autoTranslate,
281
+ branch,
282
+ devLanguage: config.devLanguage
281
283
  });
282
284
  if (deleteUnusedKeys$1) await deleteUnusedKeys(devLanguageUploadId, branch);
283
285
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["pc","csvFilesByLanguage: Record<LanguageName, CsvFile>","path","translations: TranslationsByLanguage","defaultValues: TranslationFileContents","phraseTranslations: TranslationsByLanguage","deleteUnusedKeys","phraseDeleteUnusedKeys"],"sources":["../src/file.ts","../src/logger.ts","../src/csv.ts","../src/phrase-api.ts","../src/pull-translations.ts","../src/push-translations.ts"],"sourcesContent":["import { promises as fs } from 'fs';\n\nexport const mkdir = fs.mkdir;\nexport const writeFile = fs.writeFile;\n","import pc from 'picocolors';\nimport debug from 'debug';\n\nexport const trace = debug(`vocab:phrase`);\n\nexport const log = (...params: unknown[]) => {\n // eslint-disable-next-line no-console\n console.log(pc.yellow('Vocab'), ...params);\n};\n","import { stringify } from 'csv-stringify/sync';\nimport type { LanguageName, TranslationsByLanguage } from '@vocab/core';\n\ntype Value = string | undefined;\ntype CsvRow = Value[];\ntype CsvFile = CsvRow[];\n\nexport function translationsToCsv(\n translations: TranslationsByLanguage,\n devLanguage: string,\n) {\n const languages = Object.keys(translations);\n const altLanguages = languages.filter((language) => language !== devLanguage);\n\n const devLanguageTranslations = translations[devLanguage];\n\n const csvFilesByLanguage: Record<LanguageName, CsvFile> = Object.fromEntries(\n languages.map((language) => [language, []]),\n );\n\n Object.entries(devLanguageTranslations).map(\n ([key, { message, description, tags }]) => {\n const sharedData = [key, description, tags?.join(',')];\n const devLanguageRow = [...sharedData, message];\n csvFilesByLanguage[devLanguage].push(devLanguageRow);\n\n altLanguages.map((language) => {\n const altTranslationMessage = translations[language]?.[key]?.message;\n\n if (altTranslationMessage) {\n csvFilesByLanguage[language].push([\n ...sharedData,\n altTranslationMessage,\n ]);\n }\n });\n },\n );\n\n const csvFileStrings = Object.fromEntries(\n Object.entries(csvFilesByLanguage)\n // Ensure CSV files are only created if the language has at least 1 translation\n .filter(([_, csvFile]) => csvFile.length > 0)\n .map(([language, csvFile]) => {\n const csvFileString = stringify(csvFile, {\n delimiter: ',',\n header: false,\n });\n\n return [language, csvFileString];\n }),\n );\n\n // Column indices start at 1\n const keyIndex = 1;\n const commentIndex = keyIndex + 1;\n const tagColumn = commentIndex + 1;\n const messageIndex = tagColumn + 1;\n\n return { csvFileStrings, keyIndex, messageIndex, commentIndex, tagColumn };\n}\n","/* eslint-disable no-console */\nimport type { TranslationsByLanguage } from '@vocab/core';\nimport { log, trace } from './logger';\nimport { translationsToCsv } from './csv';\n\nfunction _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {\n const phraseApiToken = process.env.PHRASE_API_TOKEN;\n\n if (!phraseApiToken) {\n throw new Error('Missing PHRASE_API_TOKEN');\n }\n\n return fetch(path, {\n ...options,\n headers: {\n Authorization: `token ${phraseApiToken}`,\n // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent\n 'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',\n ...options.headers,\n },\n }).then(async (response) => {\n console.log(`${path}: ${response.status} - ${response.statusText}`);\n\n const secondsUntilLimitReset = Math.ceil(\n Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') -\n Date.now() / 1000,\n );\n console.log(\n `Rate Limit: ${response.headers.get(\n 'X-Rate-Limit-Remaining',\n )} of ${response.headers.get(\n 'X-Rate-Limit-Limit',\n )} remaining. (${secondsUntilLimitReset} seconds remaining)`,\n );\n\n trace('\\nLink:', response.headers.get('Link'), '\\n');\n // Print All Headers:\n // console.log(Array.from(r.headers.entries()));\n\n try {\n const result = await response.json();\n\n trace(`Internal Result (Length: ${result.length})\\n`);\n\n if (\n (!options.method || options.method === 'GET') &&\n response.headers.get('Link')?.includes('rel=next')\n ) {\n const [, nextPageUrl] =\n response.headers.get('Link')?.match(/<([^>]*)>; rel=next/) ?? [];\n\n if (!nextPageUrl) {\n throw new Error(\"Can't parse next page URL\");\n }\n\n console.log('Results received with next page: ', nextPageUrl);\n\n const nextPageResult = (await _callPhrase(nextPageUrl, options)) as any;\n\n return [...result, ...nextPageResult];\n }\n\n return result;\n } catch (e) {\n console.error('Unable to parse response as JSON', e);\n return response.text();\n }\n });\n}\n\nexport async function callPhrase<T = any>(\n relativePath: string,\n options: Parameters<typeof fetch>[1] = {},\n): Promise<T> {\n const projectId = process.env.PHRASE_PROJECT_ID;\n\n if (!projectId) {\n throw new Error('Missing PHRASE_PROJECT_ID');\n }\n return _callPhrase(\n `https://api.phrase.com/v2/projects/${projectId}/${relativePath}`,\n options,\n )\n .then((result) => {\n if (Array.isArray(result)) {\n console.log('Result length:', result.length);\n }\n return result;\n })\n .catch((error) => {\n console.error(`Error calling phrase for ${relativePath}:`, error);\n throw Error;\n });\n}\n\nexport async function pullAllTranslations(\n branch: string,\n): Promise<TranslationsByLanguage> {\n const phraseResult = await callPhrase<\n Array<{\n key: { name: string };\n locale: { name: string };\n content: string;\n }>\n >(`translations?branch=${branch}&per_page=100`);\n\n const translations: TranslationsByLanguage = {};\n\n for (const r of phraseResult) {\n if (!translations[r.locale.name]) {\n translations[r.locale.name] = {};\n }\n translations[r.locale.name][r.key.name] = { message: r.content };\n }\n\n return translations;\n}\n\nexport async function pushTranslations(\n translationsByLanguage: TranslationsByLanguage,\n { devLanguage, branch }: { devLanguage: string; branch: string },\n) {\n const { csvFileStrings, keyIndex, commentIndex, tagColumn, messageIndex } =\n translationsToCsv(translationsByLanguage, devLanguage);\n\n let devLanguageUploadId = '';\n\n for (const [language, csvFileString] of Object.entries(csvFileStrings)) {\n const formData = new FormData();\n\n formData.append(\n 'file',\n new Blob([csvFileString], {\n type: 'text/csv',\n }),\n `${language}.translations.csv`,\n );\n\n formData.append('file_format', 'csv');\n formData.append('branch', branch);\n formData.append('update_translations', 'true');\n formData.append('update_descriptions', 'true');\n\n formData.append(`locale_mapping[${language}]`, messageIndex.toString());\n\n formData.append('format_options[key_index]', keyIndex.toString());\n formData.append('format_options[comment_index]', commentIndex.toString());\n formData.append('format_options[tag_column]', tagColumn.toString());\n formData.append('format_options[enable_pluralization]', 'false');\n\n log(`Uploading translations for language ${language}`);\n\n const result = await callPhrase<\n | {\n id: string;\n }\n | {\n message: string;\n errors: unknown[];\n }\n | undefined\n >(`uploads`, {\n method: 'POST',\n body: formData,\n });\n\n trace('Upload result:\\n', result);\n\n if (result && 'id' in result) {\n log('Upload ID:', result.id, '\\n');\n log('Successfully Uploaded\\n');\n } else {\n log(`Error uploading: ${result?.message}\\n`);\n log('Response:', result);\n throw new Error('Error uploading');\n }\n\n if (language === devLanguage) {\n devLanguageUploadId = result.id;\n }\n }\n\n return {\n devLanguageUploadId,\n };\n}\n\nexport async function deleteUnusedKeys(uploadId: string, branch: string) {\n const query = `unmentioned_in_upload:${uploadId}`;\n const { records_affected } = await callPhrase<{ records_affected: number }>(\n 'keys',\n {\n method: 'DELETE',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ branch, q: query }),\n },\n );\n\n log(\n 'Successfully deleted',\n records_affected,\n 'unused keys from branch',\n branch,\n );\n}\n\nexport async function ensureBranch(branch: string) {\n await callPhrase(`branches`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: branch }),\n });\n\n log('Created branch:', branch);\n}\n","import { writeFile, mkdir } from './file';\nimport path from 'path';\n\nimport {\n type TranslationFileContents,\n type UserConfig,\n loadAllTranslations,\n getAltLanguageFilePath,\n getAltLanguages,\n getUniqueKey,\n} from '@vocab/core';\n\nimport { pullAllTranslations, ensureBranch } from './phrase-api';\nimport { trace } from './logger';\n\ninterface PullOptions {\n branch?: string;\n deleteUnusedKeys?: boolean;\n errorOnNoGlobalKeyTranslation?: boolean;\n}\n\nexport async function pull(\n { branch = 'local-development', errorOnNoGlobalKeyTranslation }: PullOptions,\n config: UserConfig,\n) {\n trace(`Pulling translations from branch ${branch}`);\n await ensureBranch(branch);\n const alternativeLanguages = getAltLanguages(config);\n const allPhraseTranslations = await pullAllTranslations(branch);\n trace(\n `Pulling translations from Phrase for languages ${\n config.devLanguage\n } and ${alternativeLanguages.join(', ')}`,\n );\n\n const phraseLanguages = Object.keys(allPhraseTranslations);\n trace(\n `Found Phrase translations for languages ${phraseLanguages.join(', ')}`,\n );\n\n if (!phraseLanguages.includes(config.devLanguage)) {\n throw new Error(\n `Phrase did not return any translations for the configured development language \"${config.devLanguage}\".\\nPlease ensure this language is present in your Phrase project's configuration.`,\n );\n }\n\n const allVocabTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n config,\n );\n\n for (const loadedTranslation of allVocabTranslations) {\n const devTranslations = loadedTranslation.languages[config.devLanguage];\n\n if (!devTranslations) {\n throw new Error('No dev language translations loaded');\n }\n\n const defaultValues: TranslationFileContents = { ...devTranslations };\n const localKeys = Object.keys(defaultValues);\n\n for (const key of localKeys) {\n defaultValues[key] = {\n ...defaultValues[key],\n ...allPhraseTranslations[config.devLanguage][\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace)\n ],\n };\n }\n\n // Only write a `_meta` field if necessary\n if (Object.keys(loadedTranslation.metadata).length > 0) {\n defaultValues._meta = loadedTranslation.metadata;\n }\n\n await writeFile(\n loadedTranslation.filePath,\n `${JSON.stringify(defaultValues, null, 2)}\\n`,\n );\n\n for (const alternativeLanguage of alternativeLanguages) {\n if (alternativeLanguage in allPhraseTranslations) {\n const altTranslations = {\n ...loadedTranslation.languages[alternativeLanguage],\n };\n const phraseAltTranslations =\n allPhraseTranslations[alternativeLanguage];\n\n for (const key of localKeys) {\n const phraseKey =\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace);\n const phraseTranslationMessage =\n phraseAltTranslations[phraseKey]?.message;\n\n if (!phraseTranslationMessage) {\n trace(\n `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,\n );\n if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {\n throw new Error(\n `Missing translation for global key ${key} in language ${alternativeLanguage}`,\n );\n }\n continue;\n }\n\n altTranslations[key] = {\n ...altTranslations[key],\n message: phraseTranslationMessage,\n };\n }\n\n const altTranslationFilePath = getAltLanguageFilePath(\n loadedTranslation.filePath,\n alternativeLanguage,\n );\n\n await mkdir(path.dirname(altTranslationFilePath), {\n recursive: true,\n });\n await writeFile(\n altTranslationFilePath,\n `${JSON.stringify(altTranslations, null, 2)}\\n`,\n );\n }\n }\n }\n}\n","import {\n loadAllTranslations,\n getUniqueKey,\n type TranslationData,\n type TranslationsByLanguage,\n type UserConfig,\n} from '@vocab/core';\nimport {\n ensureBranch,\n deleteUnusedKeys as phraseDeleteUnusedKeys,\n pushTranslations,\n} from './phrase-api';\nimport { trace } from './logger';\n\ninterface PushOptions {\n branch: string;\n deleteUnusedKeys?: boolean;\n ignore?: string[];\n}\n\n/**\n * Uploads translations to the Phrase API for each language.\n * A unique namespace is appended to each key using the file path the key came from.\n */\nexport async function push(\n { branch, deleteUnusedKeys, ignore }: PushOptions,\n config: UserConfig,\n) {\n if (ignore) {\n trace(`ignoring files on paths: ${ignore.join(', ')}`);\n }\n const allLanguageTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n {\n ...config,\n ignore: [...(config.ignore || []), ...(ignore || [])],\n },\n );\n trace(`Pushing translations to branch ${branch}`);\n const allLanguages = config.languages.map((v) => v.name);\n await ensureBranch(branch);\n\n trace(\n `Pushing translations to phrase for languages ${allLanguages.join(', ')}`,\n );\n\n const phraseTranslations: TranslationsByLanguage = {};\n\n for (const loadedTranslation of allLanguageTranslations) {\n for (const language of allLanguages) {\n const localTranslations = loadedTranslation.languages[language];\n if (!localTranslations) {\n continue;\n }\n if (!phraseTranslations[language]) {\n phraseTranslations[language] = {};\n }\n\n const {\n metadata: { tags: sharedTags = [] },\n } = loadedTranslation;\n\n for (const localKey of Object.keys(localTranslations)) {\n const { tags = [], ...localTranslation } = localTranslations[localKey];\n if (language === config.devLanguage) {\n (localTranslation as TranslationData).tags = [...tags, ...sharedTags];\n }\n const globalKey =\n loadedTranslation.languages[config.devLanguage][localKey].globalKey;\n\n const phraseKey =\n globalKey ?? getUniqueKey(localKey, loadedTranslation.namespace);\n\n phraseTranslations[language][phraseKey] = localTranslation;\n }\n }\n }\n\n const { devLanguageUploadId } = await pushTranslations(phraseTranslations, {\n devLanguage: config.devLanguage,\n branch,\n });\n\n if (deleteUnusedKeys) {\n await phraseDeleteUnusedKeys(devLanguageUploadId, branch);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["pc","csvFilesByLanguage: Record<LanguageName, CsvFile>","path","translations: TranslationsByLanguage","defaultValues: TranslationFileContents","phraseTranslations: TranslationsByLanguage","deleteUnusedKeys","phraseDeleteUnusedKeys"],"sources":["../src/file.ts","../src/logger.ts","../src/csv.ts","../src/phrase-api.ts","../src/pull-translations.ts","../src/push-translations.ts"],"sourcesContent":["import { promises as fs } from 'fs';\n\nexport const mkdir = fs.mkdir;\nexport const writeFile = fs.writeFile;\n","import pc from 'picocolors';\nimport debug from 'debug';\n\nexport const trace = debug(`vocab:phrase`);\n\nexport const log = (...params: unknown[]) => {\n // eslint-disable-next-line no-console\n console.log(pc.yellow('Vocab'), ...params);\n};\n","import { stringify } from 'csv-stringify/sync';\nimport type { LanguageName, TranslationsByLanguage } from '@vocab/core';\n\ntype Value = string | undefined;\ntype CsvRow = Value[];\ntype CsvFile = CsvRow[];\n\nexport function translationsToCsv(\n translations: TranslationsByLanguage,\n devLanguage: string,\n) {\n const languages = Object.keys(translations);\n const altLanguages = languages.filter((language) => language !== devLanguage);\n\n const devLanguageTranslations = translations[devLanguage];\n\n const csvFilesByLanguage: Record<LanguageName, CsvFile> = Object.fromEntries(\n languages.map((language) => [language, []]),\n );\n\n Object.entries(devLanguageTranslations).map(\n ([key, { message, description, tags }]) => {\n const sharedData = [key, description, tags?.join(',')];\n const devLanguageRow = [...sharedData, message];\n csvFilesByLanguage[devLanguage].push(devLanguageRow);\n\n altLanguages.map((language) => {\n const altTranslationMessage = translations[language]?.[key]?.message;\n\n if (altTranslationMessage) {\n csvFilesByLanguage[language].push([\n ...sharedData,\n altTranslationMessage,\n ]);\n }\n });\n },\n );\n\n const csvFileStrings = Object.fromEntries(\n Object.entries(csvFilesByLanguage)\n // Ensure CSV files are only created if the language has at least 1 translation\n .filter(([_, csvFile]) => csvFile.length > 0)\n .map(([language, csvFile]) => {\n const csvFileString = stringify(csvFile, {\n delimiter: ',',\n header: false,\n });\n\n return [language, csvFileString];\n }),\n );\n\n // Column indices start at 1\n const keyIndex = 1;\n const commentIndex = keyIndex + 1;\n const tagColumn = commentIndex + 1;\n const messageIndex = tagColumn + 1;\n\n return { csvFileStrings, keyIndex, messageIndex, commentIndex, tagColumn };\n}\n","/* eslint-disable no-console */\nimport type { TranslationsByLanguage } from '@vocab/core';\nimport { log, trace } from './logger';\nimport { translationsToCsv } from './csv';\n\nfunction _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {\n const phraseApiToken = process.env.PHRASE_API_TOKEN;\n\n if (!phraseApiToken) {\n throw new Error('Missing PHRASE_API_TOKEN');\n }\n\n return fetch(path, {\n ...options,\n headers: {\n Authorization: `token ${phraseApiToken}`,\n // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent\n 'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',\n ...options.headers,\n },\n }).then(async (response) => {\n console.log(`${path}: ${response.status} - ${response.statusText}`);\n\n const secondsUntilLimitReset = Math.ceil(\n Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') -\n Date.now() / 1000,\n );\n console.log(\n `Rate Limit: ${response.headers.get(\n 'X-Rate-Limit-Remaining',\n )} of ${response.headers.get(\n 'X-Rate-Limit-Limit',\n )} remaining. (${secondsUntilLimitReset} seconds remaining)`,\n );\n\n trace('\\nLink:', response.headers.get('Link'), '\\n');\n // Print All Headers:\n // console.log(Array.from(r.headers.entries()));\n\n try {\n const result = await response.json();\n\n trace(`Internal Result (Length: ${result.length})\\n`);\n\n if (\n (!options.method || options.method === 'GET') &&\n response.headers.get('Link')?.includes('rel=next')\n ) {\n const [, nextPageUrl] =\n response.headers.get('Link')?.match(/<([^>]*)>; rel=next/) ?? [];\n\n if (!nextPageUrl) {\n throw new Error(\"Can't parse next page URL\");\n }\n\n console.log('Results received with next page: ', nextPageUrl);\n\n const nextPageResult = (await _callPhrase(nextPageUrl, options)) as any;\n\n return [...result, ...nextPageResult];\n }\n\n return result;\n } catch (e) {\n console.error('Unable to parse response as JSON', e);\n return response.text();\n }\n });\n}\n\nexport async function callPhrase<T = any>(\n relativePath: string,\n options: Parameters<typeof fetch>[1] = {},\n): Promise<T> {\n const projectId = process.env.PHRASE_PROJECT_ID;\n\n if (!projectId) {\n throw new Error('Missing PHRASE_PROJECT_ID');\n }\n return _callPhrase(\n `https://api.phrase.com/v2/projects/${projectId}/${relativePath}`,\n options,\n )\n .then((result) => {\n if (Array.isArray(result)) {\n console.log('Result length:', result.length);\n }\n return result;\n })\n .catch((error) => {\n console.error(`Error calling phrase for ${relativePath}:`, error);\n throw Error;\n });\n}\n\nexport async function pullAllTranslations(\n branch: string,\n): Promise<TranslationsByLanguage> {\n const phraseResult = await callPhrase<\n Array<{\n key: { name: string };\n locale: { name: string };\n content: string;\n }>\n >(`translations?branch=${branch}&per_page=100`);\n\n const translations: TranslationsByLanguage = {};\n\n for (const r of phraseResult) {\n if (!translations[r.locale.name]) {\n translations[r.locale.name] = {};\n }\n translations[r.locale.name][r.key.name] = { message: r.content };\n }\n\n return translations;\n}\n\nexport async function pushTranslations(\n translationsByLanguage: TranslationsByLanguage,\n {\n autoTranslate,\n branch,\n devLanguage,\n }: { autoTranslate?: boolean; branch: string; devLanguage: string },\n) {\n const { csvFileStrings, keyIndex, commentIndex, tagColumn, messageIndex } =\n translationsToCsv(translationsByLanguage, devLanguage);\n\n let devLanguageUploadId = '';\n\n for (const [language, csvFileString] of Object.entries(csvFileStrings)) {\n const formData = new FormData();\n\n formData.append(\n 'file',\n new Blob([csvFileString], {\n type: 'text/csv',\n }),\n `${language}.translations.csv`,\n );\n\n formData.append('file_format', 'csv');\n formData.append('branch', branch);\n formData.append('update_translations', 'true');\n formData.append('update_descriptions', 'true');\n\n if (autoTranslate) {\n formData.append('autotranslate', 'true');\n }\n\n formData.append(`locale_mapping[${language}]`, messageIndex.toString());\n\n formData.append('format_options[key_index]', keyIndex.toString());\n formData.append('format_options[comment_index]', commentIndex.toString());\n formData.append('format_options[tag_column]', tagColumn.toString());\n formData.append('format_options[enable_pluralization]', 'false');\n\n log(`Uploading translations for language ${language}`);\n\n const result = await callPhrase<\n | {\n id: string;\n }\n | {\n message: string;\n errors: unknown[];\n }\n | undefined\n >(`uploads`, {\n method: 'POST',\n body: formData,\n });\n\n trace('Upload result:\\n', result);\n\n if (result && 'id' in result) {\n log('Upload ID:', result.id, '\\n');\n log('Successfully Uploaded\\n');\n } else {\n log(`Error uploading: ${result?.message}\\n`);\n log('Response:', result);\n throw new Error('Error uploading');\n }\n\n if (language === devLanguage) {\n devLanguageUploadId = result.id;\n }\n }\n\n return {\n devLanguageUploadId,\n };\n}\n\nexport async function deleteUnusedKeys(uploadId: string, branch: string) {\n const query = `unmentioned_in_upload:${uploadId}`;\n const { records_affected } = await callPhrase<{ records_affected: number }>(\n 'keys',\n {\n method: 'DELETE',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ branch, q: query }),\n },\n );\n\n log(\n 'Successfully deleted',\n records_affected,\n 'unused keys from branch',\n branch,\n );\n}\n\nexport async function ensureBranch(branch: string) {\n await callPhrase(`branches`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: branch }),\n });\n\n log('Created branch:', branch);\n}\n","import { writeFile, mkdir } from './file';\nimport path from 'path';\n\nimport {\n type TranslationFileContents,\n type UserConfig,\n loadAllTranslations,\n getAltLanguageFilePath,\n getAltLanguages,\n getUniqueKey,\n} from '@vocab/core';\n\nimport { pullAllTranslations, ensureBranch } from './phrase-api';\nimport { trace } from './logger';\n\ninterface PullOptions {\n branch?: string;\n deleteUnusedKeys?: boolean;\n errorOnNoGlobalKeyTranslation?: boolean;\n}\n\nexport async function pull(\n { branch = 'local-development', errorOnNoGlobalKeyTranslation }: PullOptions,\n config: UserConfig,\n) {\n trace(`Pulling translations from branch ${branch}`);\n await ensureBranch(branch);\n const alternativeLanguages = getAltLanguages(config);\n const allPhraseTranslations = await pullAllTranslations(branch);\n trace(\n `Pulling translations from Phrase for languages ${\n config.devLanguage\n } and ${alternativeLanguages.join(', ')}`,\n );\n\n const phraseLanguages = Object.keys(allPhraseTranslations);\n trace(\n `Found Phrase translations for languages ${phraseLanguages.join(', ')}`,\n );\n\n if (!phraseLanguages.includes(config.devLanguage)) {\n throw new Error(\n `Phrase did not return any translations for the configured development language \"${config.devLanguage}\".\\nPlease ensure this language is present in your Phrase project's configuration.`,\n );\n }\n\n const allVocabTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n config,\n );\n\n for (const loadedTranslation of allVocabTranslations) {\n const devTranslations = loadedTranslation.languages[config.devLanguage];\n\n if (!devTranslations) {\n throw new Error('No dev language translations loaded');\n }\n\n const defaultValues: TranslationFileContents = { ...devTranslations };\n const localKeys = Object.keys(defaultValues);\n\n for (const key of localKeys) {\n defaultValues[key] = {\n ...defaultValues[key],\n ...allPhraseTranslations[config.devLanguage][\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace)\n ],\n };\n }\n\n // Only write a `_meta` field if necessary\n if (Object.keys(loadedTranslation.metadata).length > 0) {\n defaultValues._meta = loadedTranslation.metadata;\n }\n\n await writeFile(\n loadedTranslation.filePath,\n `${JSON.stringify(defaultValues, null, 2)}\\n`,\n );\n\n for (const alternativeLanguage of alternativeLanguages) {\n if (alternativeLanguage in allPhraseTranslations) {\n const altTranslations = {\n ...loadedTranslation.languages[alternativeLanguage],\n };\n const phraseAltTranslations =\n allPhraseTranslations[alternativeLanguage];\n\n for (const key of localKeys) {\n const phraseKey =\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace);\n const phraseTranslationMessage =\n phraseAltTranslations[phraseKey]?.message;\n\n if (!phraseTranslationMessage) {\n trace(\n `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,\n );\n if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {\n throw new Error(\n `Missing translation for global key ${key} in language ${alternativeLanguage}`,\n );\n }\n continue;\n }\n\n altTranslations[key] = {\n ...altTranslations[key],\n message: phraseTranslationMessage,\n };\n }\n\n const altTranslationFilePath = getAltLanguageFilePath(\n loadedTranslation.filePath,\n alternativeLanguage,\n );\n\n await mkdir(path.dirname(altTranslationFilePath), {\n recursive: true,\n });\n await writeFile(\n altTranslationFilePath,\n `${JSON.stringify(altTranslations, null, 2)}\\n`,\n );\n }\n }\n }\n}\n","import {\n loadAllTranslations,\n getUniqueKey,\n type TranslationData,\n type TranslationsByLanguage,\n type UserConfig,\n} from '@vocab/core';\nimport {\n ensureBranch,\n deleteUnusedKeys as phraseDeleteUnusedKeys,\n pushTranslations,\n} from './phrase-api';\nimport { trace } from './logger';\n\ninterface PushOptions {\n autoTranslate?: boolean;\n branch: string;\n deleteUnusedKeys?: boolean;\n ignore?: string[];\n}\n\n/**\n * Uploads translations to the Phrase API for each language.\n * A unique namespace is appended to each key using the file path the key came from.\n */\nexport async function push(\n { autoTranslate, branch, deleteUnusedKeys, ignore }: PushOptions,\n config: UserConfig,\n) {\n if (ignore) {\n trace(`ignoring files on paths: ${ignore.join(', ')}`);\n }\n const allLanguageTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n {\n ...config,\n ignore: [...(config.ignore || []), ...(ignore || [])],\n },\n );\n trace(`Pushing translations to branch ${branch}`);\n const allLanguages = config.languages.map((v) => v.name);\n await ensureBranch(branch);\n\n trace(\n `Pushing translations to phrase for languages ${allLanguages.join(', ')}`,\n );\n\n const phraseTranslations: TranslationsByLanguage = {};\n\n for (const loadedTranslation of allLanguageTranslations) {\n for (const language of allLanguages) {\n const localTranslations = loadedTranslation.languages[language];\n if (!localTranslations) {\n continue;\n }\n if (!phraseTranslations[language]) {\n phraseTranslations[language] = {};\n }\n\n const {\n metadata: { tags: sharedTags = [] },\n } = loadedTranslation;\n\n for (const localKey of Object.keys(localTranslations)) {\n const { tags = [], ...localTranslation } = localTranslations[localKey];\n if (language === config.devLanguage) {\n (localTranslation as TranslationData).tags = [...tags, ...sharedTags];\n }\n const globalKey =\n loadedTranslation.languages[config.devLanguage][localKey].globalKey;\n\n const phraseKey =\n globalKey ?? getUniqueKey(localKey, loadedTranslation.namespace);\n\n phraseTranslations[language][phraseKey] = localTranslation;\n }\n }\n }\n\n const { devLanguageUploadId } = await pushTranslations(phraseTranslations, {\n autoTranslate,\n branch,\n devLanguage: config.devLanguage,\n });\n\n if (deleteUnusedKeys) {\n await phraseDeleteUnusedKeys(devLanguageUploadId, branch);\n }\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -13,6 +13,7 @@ declare function pull({
13
13
  //#endregion
14
14
  //#region src/push-translations.d.ts
15
15
  interface PushOptions {
16
+ autoTranslate?: boolean;
16
17
  branch: string;
17
18
  deleteUnusedKeys?: boolean;
18
19
  ignore?: string[];
@@ -22,6 +23,7 @@ interface PushOptions {
22
23
  * A unique namespace is appended to each key using the file path the key came from.
23
24
  */
24
25
  declare function push({
26
+ autoTranslate,
25
27
  branch,
26
28
  deleteUnusedKeys,
27
29
  ignore
package/dist/index.d.mts CHANGED
@@ -13,6 +13,7 @@ declare function pull({
13
13
  //#endregion
14
14
  //#region src/push-translations.d.ts
15
15
  interface PushOptions {
16
+ autoTranslate?: boolean;
16
17
  branch: string;
17
18
  deleteUnusedKeys?: boolean;
18
19
  ignore?: string[];
@@ -22,6 +23,7 @@ interface PushOptions {
22
23
  * A unique namespace is appended to each key using the file path the key came from.
23
24
  */
24
25
  declare function push({
26
+ autoTranslate,
25
27
  branch,
26
28
  deleteUnusedKeys,
27
29
  ignore
package/dist/index.mjs CHANGED
@@ -108,7 +108,7 @@ async function pullAllTranslations(branch) {
108
108
  }
109
109
  return translations;
110
110
  }
111
- async function pushTranslations(translationsByLanguage, { devLanguage, branch }) {
111
+ async function pushTranslations(translationsByLanguage, { autoTranslate, branch, devLanguage }) {
112
112
  const { csvFileStrings, keyIndex, commentIndex, tagColumn, messageIndex } = translationsToCsv(translationsByLanguage, devLanguage);
113
113
  let devLanguageUploadId = "";
114
114
  for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
@@ -118,6 +118,7 @@ async function pushTranslations(translationsByLanguage, { devLanguage, branch })
118
118
  formData.append("branch", branch);
119
119
  formData.append("update_translations", "true");
120
120
  formData.append("update_descriptions", "true");
121
+ if (autoTranslate) formData.append("autotranslate", "true");
121
122
  formData.append(`locale_mapping[${language}]`, messageIndex.toString());
122
123
  formData.append("format_options[key_index]", keyIndex.toString());
123
124
  formData.append("format_options[comment_index]", commentIndex.toString());
@@ -218,7 +219,7 @@ async function pull({ branch = "local-development", errorOnNoGlobalKeyTranslatio
218
219
  * Uploads translations to the Phrase API for each language.
219
220
  * A unique namespace is appended to each key using the file path the key came from.
220
221
  */
221
- async function push({ branch, deleteUnusedKeys: deleteUnusedKeys$1, ignore }, config) {
222
+ async function push({ autoTranslate, branch, deleteUnusedKeys: deleteUnusedKeys$1, ignore }, config) {
222
223
  if (ignore) trace(`ignoring files on paths: ${ignore.join(", ")}`);
223
224
  const allLanguageTranslations = await loadAllTranslations({
224
225
  fallbacks: "none",
@@ -246,8 +247,9 @@ async function push({ branch, deleteUnusedKeys: deleteUnusedKeys$1, ignore }, co
246
247
  }
247
248
  }
248
249
  const { devLanguageUploadId } = await pushTranslations(phraseTranslations, {
249
- devLanguage: config.devLanguage,
250
- branch
250
+ autoTranslate,
251
+ branch,
252
+ devLanguage: config.devLanguage
251
253
  });
252
254
  if (deleteUnusedKeys$1) await deleteUnusedKeys(devLanguageUploadId, branch);
253
255
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["fs","csvFilesByLanguage: Record<LanguageName, CsvFile>","path","translations: TranslationsByLanguage","defaultValues: TranslationFileContents","phraseTranslations: TranslationsByLanguage","deleteUnusedKeys","phraseDeleteUnusedKeys"],"sources":["../src/file.ts","../src/logger.ts","../src/csv.ts","../src/phrase-api.ts","../src/pull-translations.ts","../src/push-translations.ts"],"sourcesContent":["import { promises as fs } from 'fs';\n\nexport const mkdir = fs.mkdir;\nexport const writeFile = fs.writeFile;\n","import pc from 'picocolors';\nimport debug from 'debug';\n\nexport const trace = debug(`vocab:phrase`);\n\nexport const log = (...params: unknown[]) => {\n // eslint-disable-next-line no-console\n console.log(pc.yellow('Vocab'), ...params);\n};\n","import { stringify } from 'csv-stringify/sync';\nimport type { LanguageName, TranslationsByLanguage } from '@vocab/core';\n\ntype Value = string | undefined;\ntype CsvRow = Value[];\ntype CsvFile = CsvRow[];\n\nexport function translationsToCsv(\n translations: TranslationsByLanguage,\n devLanguage: string,\n) {\n const languages = Object.keys(translations);\n const altLanguages = languages.filter((language) => language !== devLanguage);\n\n const devLanguageTranslations = translations[devLanguage];\n\n const csvFilesByLanguage: Record<LanguageName, CsvFile> = Object.fromEntries(\n languages.map((language) => [language, []]),\n );\n\n Object.entries(devLanguageTranslations).map(\n ([key, { message, description, tags }]) => {\n const sharedData = [key, description, tags?.join(',')];\n const devLanguageRow = [...sharedData, message];\n csvFilesByLanguage[devLanguage].push(devLanguageRow);\n\n altLanguages.map((language) => {\n const altTranslationMessage = translations[language]?.[key]?.message;\n\n if (altTranslationMessage) {\n csvFilesByLanguage[language].push([\n ...sharedData,\n altTranslationMessage,\n ]);\n }\n });\n },\n );\n\n const csvFileStrings = Object.fromEntries(\n Object.entries(csvFilesByLanguage)\n // Ensure CSV files are only created if the language has at least 1 translation\n .filter(([_, csvFile]) => csvFile.length > 0)\n .map(([language, csvFile]) => {\n const csvFileString = stringify(csvFile, {\n delimiter: ',',\n header: false,\n });\n\n return [language, csvFileString];\n }),\n );\n\n // Column indices start at 1\n const keyIndex = 1;\n const commentIndex = keyIndex + 1;\n const tagColumn = commentIndex + 1;\n const messageIndex = tagColumn + 1;\n\n return { csvFileStrings, keyIndex, messageIndex, commentIndex, tagColumn };\n}\n","/* eslint-disable no-console */\nimport type { TranslationsByLanguage } from '@vocab/core';\nimport { log, trace } from './logger';\nimport { translationsToCsv } from './csv';\n\nfunction _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {\n const phraseApiToken = process.env.PHRASE_API_TOKEN;\n\n if (!phraseApiToken) {\n throw new Error('Missing PHRASE_API_TOKEN');\n }\n\n return fetch(path, {\n ...options,\n headers: {\n Authorization: `token ${phraseApiToken}`,\n // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent\n 'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',\n ...options.headers,\n },\n }).then(async (response) => {\n console.log(`${path}: ${response.status} - ${response.statusText}`);\n\n const secondsUntilLimitReset = Math.ceil(\n Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') -\n Date.now() / 1000,\n );\n console.log(\n `Rate Limit: ${response.headers.get(\n 'X-Rate-Limit-Remaining',\n )} of ${response.headers.get(\n 'X-Rate-Limit-Limit',\n )} remaining. (${secondsUntilLimitReset} seconds remaining)`,\n );\n\n trace('\\nLink:', response.headers.get('Link'), '\\n');\n // Print All Headers:\n // console.log(Array.from(r.headers.entries()));\n\n try {\n const result = await response.json();\n\n trace(`Internal Result (Length: ${result.length})\\n`);\n\n if (\n (!options.method || options.method === 'GET') &&\n response.headers.get('Link')?.includes('rel=next')\n ) {\n const [, nextPageUrl] =\n response.headers.get('Link')?.match(/<([^>]*)>; rel=next/) ?? [];\n\n if (!nextPageUrl) {\n throw new Error(\"Can't parse next page URL\");\n }\n\n console.log('Results received with next page: ', nextPageUrl);\n\n const nextPageResult = (await _callPhrase(nextPageUrl, options)) as any;\n\n return [...result, ...nextPageResult];\n }\n\n return result;\n } catch (e) {\n console.error('Unable to parse response as JSON', e);\n return response.text();\n }\n });\n}\n\nexport async function callPhrase<T = any>(\n relativePath: string,\n options: Parameters<typeof fetch>[1] = {},\n): Promise<T> {\n const projectId = process.env.PHRASE_PROJECT_ID;\n\n if (!projectId) {\n throw new Error('Missing PHRASE_PROJECT_ID');\n }\n return _callPhrase(\n `https://api.phrase.com/v2/projects/${projectId}/${relativePath}`,\n options,\n )\n .then((result) => {\n if (Array.isArray(result)) {\n console.log('Result length:', result.length);\n }\n return result;\n })\n .catch((error) => {\n console.error(`Error calling phrase for ${relativePath}:`, error);\n throw Error;\n });\n}\n\nexport async function pullAllTranslations(\n branch: string,\n): Promise<TranslationsByLanguage> {\n const phraseResult = await callPhrase<\n Array<{\n key: { name: string };\n locale: { name: string };\n content: string;\n }>\n >(`translations?branch=${branch}&per_page=100`);\n\n const translations: TranslationsByLanguage = {};\n\n for (const r of phraseResult) {\n if (!translations[r.locale.name]) {\n translations[r.locale.name] = {};\n }\n translations[r.locale.name][r.key.name] = { message: r.content };\n }\n\n return translations;\n}\n\nexport async function pushTranslations(\n translationsByLanguage: TranslationsByLanguage,\n { devLanguage, branch }: { devLanguage: string; branch: string },\n) {\n const { csvFileStrings, keyIndex, commentIndex, tagColumn, messageIndex } =\n translationsToCsv(translationsByLanguage, devLanguage);\n\n let devLanguageUploadId = '';\n\n for (const [language, csvFileString] of Object.entries(csvFileStrings)) {\n const formData = new FormData();\n\n formData.append(\n 'file',\n new Blob([csvFileString], {\n type: 'text/csv',\n }),\n `${language}.translations.csv`,\n );\n\n formData.append('file_format', 'csv');\n formData.append('branch', branch);\n formData.append('update_translations', 'true');\n formData.append('update_descriptions', 'true');\n\n formData.append(`locale_mapping[${language}]`, messageIndex.toString());\n\n formData.append('format_options[key_index]', keyIndex.toString());\n formData.append('format_options[comment_index]', commentIndex.toString());\n formData.append('format_options[tag_column]', tagColumn.toString());\n formData.append('format_options[enable_pluralization]', 'false');\n\n log(`Uploading translations for language ${language}`);\n\n const result = await callPhrase<\n | {\n id: string;\n }\n | {\n message: string;\n errors: unknown[];\n }\n | undefined\n >(`uploads`, {\n method: 'POST',\n body: formData,\n });\n\n trace('Upload result:\\n', result);\n\n if (result && 'id' in result) {\n log('Upload ID:', result.id, '\\n');\n log('Successfully Uploaded\\n');\n } else {\n log(`Error uploading: ${result?.message}\\n`);\n log('Response:', result);\n throw new Error('Error uploading');\n }\n\n if (language === devLanguage) {\n devLanguageUploadId = result.id;\n }\n }\n\n return {\n devLanguageUploadId,\n };\n}\n\nexport async function deleteUnusedKeys(uploadId: string, branch: string) {\n const query = `unmentioned_in_upload:${uploadId}`;\n const { records_affected } = await callPhrase<{ records_affected: number }>(\n 'keys',\n {\n method: 'DELETE',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ branch, q: query }),\n },\n );\n\n log(\n 'Successfully deleted',\n records_affected,\n 'unused keys from branch',\n branch,\n );\n}\n\nexport async function ensureBranch(branch: string) {\n await callPhrase(`branches`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: branch }),\n });\n\n log('Created branch:', branch);\n}\n","import { writeFile, mkdir } from './file';\nimport path from 'path';\n\nimport {\n type TranslationFileContents,\n type UserConfig,\n loadAllTranslations,\n getAltLanguageFilePath,\n getAltLanguages,\n getUniqueKey,\n} from '@vocab/core';\n\nimport { pullAllTranslations, ensureBranch } from './phrase-api';\nimport { trace } from './logger';\n\ninterface PullOptions {\n branch?: string;\n deleteUnusedKeys?: boolean;\n errorOnNoGlobalKeyTranslation?: boolean;\n}\n\nexport async function pull(\n { branch = 'local-development', errorOnNoGlobalKeyTranslation }: PullOptions,\n config: UserConfig,\n) {\n trace(`Pulling translations from branch ${branch}`);\n await ensureBranch(branch);\n const alternativeLanguages = getAltLanguages(config);\n const allPhraseTranslations = await pullAllTranslations(branch);\n trace(\n `Pulling translations from Phrase for languages ${\n config.devLanguage\n } and ${alternativeLanguages.join(', ')}`,\n );\n\n const phraseLanguages = Object.keys(allPhraseTranslations);\n trace(\n `Found Phrase translations for languages ${phraseLanguages.join(', ')}`,\n );\n\n if (!phraseLanguages.includes(config.devLanguage)) {\n throw new Error(\n `Phrase did not return any translations for the configured development language \"${config.devLanguage}\".\\nPlease ensure this language is present in your Phrase project's configuration.`,\n );\n }\n\n const allVocabTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n config,\n );\n\n for (const loadedTranslation of allVocabTranslations) {\n const devTranslations = loadedTranslation.languages[config.devLanguage];\n\n if (!devTranslations) {\n throw new Error('No dev language translations loaded');\n }\n\n const defaultValues: TranslationFileContents = { ...devTranslations };\n const localKeys = Object.keys(defaultValues);\n\n for (const key of localKeys) {\n defaultValues[key] = {\n ...defaultValues[key],\n ...allPhraseTranslations[config.devLanguage][\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace)\n ],\n };\n }\n\n // Only write a `_meta` field if necessary\n if (Object.keys(loadedTranslation.metadata).length > 0) {\n defaultValues._meta = loadedTranslation.metadata;\n }\n\n await writeFile(\n loadedTranslation.filePath,\n `${JSON.stringify(defaultValues, null, 2)}\\n`,\n );\n\n for (const alternativeLanguage of alternativeLanguages) {\n if (alternativeLanguage in allPhraseTranslations) {\n const altTranslations = {\n ...loadedTranslation.languages[alternativeLanguage],\n };\n const phraseAltTranslations =\n allPhraseTranslations[alternativeLanguage];\n\n for (const key of localKeys) {\n const phraseKey =\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace);\n const phraseTranslationMessage =\n phraseAltTranslations[phraseKey]?.message;\n\n if (!phraseTranslationMessage) {\n trace(\n `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,\n );\n if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {\n throw new Error(\n `Missing translation for global key ${key} in language ${alternativeLanguage}`,\n );\n }\n continue;\n }\n\n altTranslations[key] = {\n ...altTranslations[key],\n message: phraseTranslationMessage,\n };\n }\n\n const altTranslationFilePath = getAltLanguageFilePath(\n loadedTranslation.filePath,\n alternativeLanguage,\n );\n\n await mkdir(path.dirname(altTranslationFilePath), {\n recursive: true,\n });\n await writeFile(\n altTranslationFilePath,\n `${JSON.stringify(altTranslations, null, 2)}\\n`,\n );\n }\n }\n }\n}\n","import {\n loadAllTranslations,\n getUniqueKey,\n type TranslationData,\n type TranslationsByLanguage,\n type UserConfig,\n} from '@vocab/core';\nimport {\n ensureBranch,\n deleteUnusedKeys as phraseDeleteUnusedKeys,\n pushTranslations,\n} from './phrase-api';\nimport { trace } from './logger';\n\ninterface PushOptions {\n branch: string;\n deleteUnusedKeys?: boolean;\n ignore?: string[];\n}\n\n/**\n * Uploads translations to the Phrase API for each language.\n * A unique namespace is appended to each key using the file path the key came from.\n */\nexport async function push(\n { branch, deleteUnusedKeys, ignore }: PushOptions,\n config: UserConfig,\n) {\n if (ignore) {\n trace(`ignoring files on paths: ${ignore.join(', ')}`);\n }\n const allLanguageTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n {\n ...config,\n ignore: [...(config.ignore || []), ...(ignore || [])],\n },\n );\n trace(`Pushing translations to branch ${branch}`);\n const allLanguages = config.languages.map((v) => v.name);\n await ensureBranch(branch);\n\n trace(\n `Pushing translations to phrase for languages ${allLanguages.join(', ')}`,\n );\n\n const phraseTranslations: TranslationsByLanguage = {};\n\n for (const loadedTranslation of allLanguageTranslations) {\n for (const language of allLanguages) {\n const localTranslations = loadedTranslation.languages[language];\n if (!localTranslations) {\n continue;\n }\n if (!phraseTranslations[language]) {\n phraseTranslations[language] = {};\n }\n\n const {\n metadata: { tags: sharedTags = [] },\n } = loadedTranslation;\n\n for (const localKey of Object.keys(localTranslations)) {\n const { tags = [], ...localTranslation } = localTranslations[localKey];\n if (language === config.devLanguage) {\n (localTranslation as TranslationData).tags = [...tags, ...sharedTags];\n }\n const globalKey =\n loadedTranslation.languages[config.devLanguage][localKey].globalKey;\n\n const phraseKey =\n globalKey ?? getUniqueKey(localKey, loadedTranslation.namespace);\n\n phraseTranslations[language][phraseKey] = localTranslation;\n }\n }\n }\n\n const { devLanguageUploadId } = await pushTranslations(phraseTranslations, {\n devLanguage: config.devLanguage,\n branch,\n });\n\n if (deleteUnusedKeys) {\n await phraseDeleteUnusedKeys(devLanguageUploadId, branch);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["fs","csvFilesByLanguage: Record<LanguageName, CsvFile>","path","translations: TranslationsByLanguage","defaultValues: TranslationFileContents","phraseTranslations: TranslationsByLanguage","deleteUnusedKeys","phraseDeleteUnusedKeys"],"sources":["../src/file.ts","../src/logger.ts","../src/csv.ts","../src/phrase-api.ts","../src/pull-translations.ts","../src/push-translations.ts"],"sourcesContent":["import { promises as fs } from 'fs';\n\nexport const mkdir = fs.mkdir;\nexport const writeFile = fs.writeFile;\n","import pc from 'picocolors';\nimport debug from 'debug';\n\nexport const trace = debug(`vocab:phrase`);\n\nexport const log = (...params: unknown[]) => {\n // eslint-disable-next-line no-console\n console.log(pc.yellow('Vocab'), ...params);\n};\n","import { stringify } from 'csv-stringify/sync';\nimport type { LanguageName, TranslationsByLanguage } from '@vocab/core';\n\ntype Value = string | undefined;\ntype CsvRow = Value[];\ntype CsvFile = CsvRow[];\n\nexport function translationsToCsv(\n translations: TranslationsByLanguage,\n devLanguage: string,\n) {\n const languages = Object.keys(translations);\n const altLanguages = languages.filter((language) => language !== devLanguage);\n\n const devLanguageTranslations = translations[devLanguage];\n\n const csvFilesByLanguage: Record<LanguageName, CsvFile> = Object.fromEntries(\n languages.map((language) => [language, []]),\n );\n\n Object.entries(devLanguageTranslations).map(\n ([key, { message, description, tags }]) => {\n const sharedData = [key, description, tags?.join(',')];\n const devLanguageRow = [...sharedData, message];\n csvFilesByLanguage[devLanguage].push(devLanguageRow);\n\n altLanguages.map((language) => {\n const altTranslationMessage = translations[language]?.[key]?.message;\n\n if (altTranslationMessage) {\n csvFilesByLanguage[language].push([\n ...sharedData,\n altTranslationMessage,\n ]);\n }\n });\n },\n );\n\n const csvFileStrings = Object.fromEntries(\n Object.entries(csvFilesByLanguage)\n // Ensure CSV files are only created if the language has at least 1 translation\n .filter(([_, csvFile]) => csvFile.length > 0)\n .map(([language, csvFile]) => {\n const csvFileString = stringify(csvFile, {\n delimiter: ',',\n header: false,\n });\n\n return [language, csvFileString];\n }),\n );\n\n // Column indices start at 1\n const keyIndex = 1;\n const commentIndex = keyIndex + 1;\n const tagColumn = commentIndex + 1;\n const messageIndex = tagColumn + 1;\n\n return { csvFileStrings, keyIndex, messageIndex, commentIndex, tagColumn };\n}\n","/* eslint-disable no-console */\nimport type { TranslationsByLanguage } from '@vocab/core';\nimport { log, trace } from './logger';\nimport { translationsToCsv } from './csv';\n\nfunction _callPhrase(path: string, options: Parameters<typeof fetch>[1] = {}) {\n const phraseApiToken = process.env.PHRASE_API_TOKEN;\n\n if (!phraseApiToken) {\n throw new Error('Missing PHRASE_API_TOKEN');\n }\n\n return fetch(path, {\n ...options,\n headers: {\n Authorization: `token ${phraseApiToken}`,\n // Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent\n 'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',\n ...options.headers,\n },\n }).then(async (response) => {\n console.log(`${path}: ${response.status} - ${response.statusText}`);\n\n const secondsUntilLimitReset = Math.ceil(\n Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') -\n Date.now() / 1000,\n );\n console.log(\n `Rate Limit: ${response.headers.get(\n 'X-Rate-Limit-Remaining',\n )} of ${response.headers.get(\n 'X-Rate-Limit-Limit',\n )} remaining. (${secondsUntilLimitReset} seconds remaining)`,\n );\n\n trace('\\nLink:', response.headers.get('Link'), '\\n');\n // Print All Headers:\n // console.log(Array.from(r.headers.entries()));\n\n try {\n const result = await response.json();\n\n trace(`Internal Result (Length: ${result.length})\\n`);\n\n if (\n (!options.method || options.method === 'GET') &&\n response.headers.get('Link')?.includes('rel=next')\n ) {\n const [, nextPageUrl] =\n response.headers.get('Link')?.match(/<([^>]*)>; rel=next/) ?? [];\n\n if (!nextPageUrl) {\n throw new Error(\"Can't parse next page URL\");\n }\n\n console.log('Results received with next page: ', nextPageUrl);\n\n const nextPageResult = (await _callPhrase(nextPageUrl, options)) as any;\n\n return [...result, ...nextPageResult];\n }\n\n return result;\n } catch (e) {\n console.error('Unable to parse response as JSON', e);\n return response.text();\n }\n });\n}\n\nexport async function callPhrase<T = any>(\n relativePath: string,\n options: Parameters<typeof fetch>[1] = {},\n): Promise<T> {\n const projectId = process.env.PHRASE_PROJECT_ID;\n\n if (!projectId) {\n throw new Error('Missing PHRASE_PROJECT_ID');\n }\n return _callPhrase(\n `https://api.phrase.com/v2/projects/${projectId}/${relativePath}`,\n options,\n )\n .then((result) => {\n if (Array.isArray(result)) {\n console.log('Result length:', result.length);\n }\n return result;\n })\n .catch((error) => {\n console.error(`Error calling phrase for ${relativePath}:`, error);\n throw Error;\n });\n}\n\nexport async function pullAllTranslations(\n branch: string,\n): Promise<TranslationsByLanguage> {\n const phraseResult = await callPhrase<\n Array<{\n key: { name: string };\n locale: { name: string };\n content: string;\n }>\n >(`translations?branch=${branch}&per_page=100`);\n\n const translations: TranslationsByLanguage = {};\n\n for (const r of phraseResult) {\n if (!translations[r.locale.name]) {\n translations[r.locale.name] = {};\n }\n translations[r.locale.name][r.key.name] = { message: r.content };\n }\n\n return translations;\n}\n\nexport async function pushTranslations(\n translationsByLanguage: TranslationsByLanguage,\n {\n autoTranslate,\n branch,\n devLanguage,\n }: { autoTranslate?: boolean; branch: string; devLanguage: string },\n) {\n const { csvFileStrings, keyIndex, commentIndex, tagColumn, messageIndex } =\n translationsToCsv(translationsByLanguage, devLanguage);\n\n let devLanguageUploadId = '';\n\n for (const [language, csvFileString] of Object.entries(csvFileStrings)) {\n const formData = new FormData();\n\n formData.append(\n 'file',\n new Blob([csvFileString], {\n type: 'text/csv',\n }),\n `${language}.translations.csv`,\n );\n\n formData.append('file_format', 'csv');\n formData.append('branch', branch);\n formData.append('update_translations', 'true');\n formData.append('update_descriptions', 'true');\n\n if (autoTranslate) {\n formData.append('autotranslate', 'true');\n }\n\n formData.append(`locale_mapping[${language}]`, messageIndex.toString());\n\n formData.append('format_options[key_index]', keyIndex.toString());\n formData.append('format_options[comment_index]', commentIndex.toString());\n formData.append('format_options[tag_column]', tagColumn.toString());\n formData.append('format_options[enable_pluralization]', 'false');\n\n log(`Uploading translations for language ${language}`);\n\n const result = await callPhrase<\n | {\n id: string;\n }\n | {\n message: string;\n errors: unknown[];\n }\n | undefined\n >(`uploads`, {\n method: 'POST',\n body: formData,\n });\n\n trace('Upload result:\\n', result);\n\n if (result && 'id' in result) {\n log('Upload ID:', result.id, '\\n');\n log('Successfully Uploaded\\n');\n } else {\n log(`Error uploading: ${result?.message}\\n`);\n log('Response:', result);\n throw new Error('Error uploading');\n }\n\n if (language === devLanguage) {\n devLanguageUploadId = result.id;\n }\n }\n\n return {\n devLanguageUploadId,\n };\n}\n\nexport async function deleteUnusedKeys(uploadId: string, branch: string) {\n const query = `unmentioned_in_upload:${uploadId}`;\n const { records_affected } = await callPhrase<{ records_affected: number }>(\n 'keys',\n {\n method: 'DELETE',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ branch, q: query }),\n },\n );\n\n log(\n 'Successfully deleted',\n records_affected,\n 'unused keys from branch',\n branch,\n );\n}\n\nexport async function ensureBranch(branch: string) {\n await callPhrase(`branches`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: branch }),\n });\n\n log('Created branch:', branch);\n}\n","import { writeFile, mkdir } from './file';\nimport path from 'path';\n\nimport {\n type TranslationFileContents,\n type UserConfig,\n loadAllTranslations,\n getAltLanguageFilePath,\n getAltLanguages,\n getUniqueKey,\n} from '@vocab/core';\n\nimport { pullAllTranslations, ensureBranch } from './phrase-api';\nimport { trace } from './logger';\n\ninterface PullOptions {\n branch?: string;\n deleteUnusedKeys?: boolean;\n errorOnNoGlobalKeyTranslation?: boolean;\n}\n\nexport async function pull(\n { branch = 'local-development', errorOnNoGlobalKeyTranslation }: PullOptions,\n config: UserConfig,\n) {\n trace(`Pulling translations from branch ${branch}`);\n await ensureBranch(branch);\n const alternativeLanguages = getAltLanguages(config);\n const allPhraseTranslations = await pullAllTranslations(branch);\n trace(\n `Pulling translations from Phrase for languages ${\n config.devLanguage\n } and ${alternativeLanguages.join(', ')}`,\n );\n\n const phraseLanguages = Object.keys(allPhraseTranslations);\n trace(\n `Found Phrase translations for languages ${phraseLanguages.join(', ')}`,\n );\n\n if (!phraseLanguages.includes(config.devLanguage)) {\n throw new Error(\n `Phrase did not return any translations for the configured development language \"${config.devLanguage}\".\\nPlease ensure this language is present in your Phrase project's configuration.`,\n );\n }\n\n const allVocabTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n config,\n );\n\n for (const loadedTranslation of allVocabTranslations) {\n const devTranslations = loadedTranslation.languages[config.devLanguage];\n\n if (!devTranslations) {\n throw new Error('No dev language translations loaded');\n }\n\n const defaultValues: TranslationFileContents = { ...devTranslations };\n const localKeys = Object.keys(defaultValues);\n\n for (const key of localKeys) {\n defaultValues[key] = {\n ...defaultValues[key],\n ...allPhraseTranslations[config.devLanguage][\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace)\n ],\n };\n }\n\n // Only write a `_meta` field if necessary\n if (Object.keys(loadedTranslation.metadata).length > 0) {\n defaultValues._meta = loadedTranslation.metadata;\n }\n\n await writeFile(\n loadedTranslation.filePath,\n `${JSON.stringify(defaultValues, null, 2)}\\n`,\n );\n\n for (const alternativeLanguage of alternativeLanguages) {\n if (alternativeLanguage in allPhraseTranslations) {\n const altTranslations = {\n ...loadedTranslation.languages[alternativeLanguage],\n };\n const phraseAltTranslations =\n allPhraseTranslations[alternativeLanguage];\n\n for (const key of localKeys) {\n const phraseKey =\n defaultValues[key].globalKey ??\n getUniqueKey(key, loadedTranslation.namespace);\n const phraseTranslationMessage =\n phraseAltTranslations[phraseKey]?.message;\n\n if (!phraseTranslationMessage) {\n trace(\n `Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`,\n );\n if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {\n throw new Error(\n `Missing translation for global key ${key} in language ${alternativeLanguage}`,\n );\n }\n continue;\n }\n\n altTranslations[key] = {\n ...altTranslations[key],\n message: phraseTranslationMessage,\n };\n }\n\n const altTranslationFilePath = getAltLanguageFilePath(\n loadedTranslation.filePath,\n alternativeLanguage,\n );\n\n await mkdir(path.dirname(altTranslationFilePath), {\n recursive: true,\n });\n await writeFile(\n altTranslationFilePath,\n `${JSON.stringify(altTranslations, null, 2)}\\n`,\n );\n }\n }\n }\n}\n","import {\n loadAllTranslations,\n getUniqueKey,\n type TranslationData,\n type TranslationsByLanguage,\n type UserConfig,\n} from '@vocab/core';\nimport {\n ensureBranch,\n deleteUnusedKeys as phraseDeleteUnusedKeys,\n pushTranslations,\n} from './phrase-api';\nimport { trace } from './logger';\n\ninterface PushOptions {\n autoTranslate?: boolean;\n branch: string;\n deleteUnusedKeys?: boolean;\n ignore?: string[];\n}\n\n/**\n * Uploads translations to the Phrase API for each language.\n * A unique namespace is appended to each key using the file path the key came from.\n */\nexport async function push(\n { autoTranslate, branch, deleteUnusedKeys, ignore }: PushOptions,\n config: UserConfig,\n) {\n if (ignore) {\n trace(`ignoring files on paths: ${ignore.join(', ')}`);\n }\n const allLanguageTranslations = await loadAllTranslations(\n { fallbacks: 'none', includeNodeModules: false, withTags: true },\n {\n ...config,\n ignore: [...(config.ignore || []), ...(ignore || [])],\n },\n );\n trace(`Pushing translations to branch ${branch}`);\n const allLanguages = config.languages.map((v) => v.name);\n await ensureBranch(branch);\n\n trace(\n `Pushing translations to phrase for languages ${allLanguages.join(', ')}`,\n );\n\n const phraseTranslations: TranslationsByLanguage = {};\n\n for (const loadedTranslation of allLanguageTranslations) {\n for (const language of allLanguages) {\n const localTranslations = loadedTranslation.languages[language];\n if (!localTranslations) {\n continue;\n }\n if (!phraseTranslations[language]) {\n phraseTranslations[language] = {};\n }\n\n const {\n metadata: { tags: sharedTags = [] },\n } = loadedTranslation;\n\n for (const localKey of Object.keys(localTranslations)) {\n const { tags = [], ...localTranslation } = localTranslations[localKey];\n if (language === config.devLanguage) {\n (localTranslation as TranslationData).tags = [...tags, ...sharedTags];\n }\n const globalKey =\n loadedTranslation.languages[config.devLanguage][localKey].globalKey;\n\n const phraseKey =\n globalKey ?? getUniqueKey(localKey, loadedTranslation.namespace);\n\n phraseTranslations[language][phraseKey] = localTranslation;\n }\n }\n }\n\n const { devLanguageUploadId } = await pushTranslations(phraseTranslations, {\n autoTranslate,\n branch,\n devLanguage: config.devLanguage,\n });\n\n if (deleteUnusedKeys) {\n await phraseDeleteUnusedKeys(devLanguageUploadId, branch);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vocab/phrase",
3
- "version": "2.1.10",
3
+ "version": "2.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/seek-oss/vocab.git",