Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@vocab/phrase

Package Overview
Dependencies
Maintainers
0
Versions
46
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@vocab/phrase - npm Package Compare versions

Comparing version 0.0.0-tags-support-2023185232 to 0.0.0-use-phrase-locale-name-20240924060647

LICENSE

4

dist/declarations/src/index.d.ts

@@ -1,2 +0,2 @@

export { pull } from './pull-translations';
export { push } from './push-translations';
export { pull } from "./pull-translations.js";
export { push } from "./push-translations.js";

@@ -1,7 +0,8 @@

import type { UserConfig } from '@vocab/types';
import { type UserConfig } from '@vocab/core';
interface PullOptions {
branch?: string;
deleteUnusedKeys?: boolean;
errorOnNoGlobalKeyTranslation?: boolean;
}
export declare function pull({ branch }: PullOptions, config: UserConfig): Promise<void>;
export declare function pull({ branch, errorOnNoGlobalKeyTranslation }: PullOptions, config: UserConfig): Promise<void>;
export {};

@@ -1,2 +0,2 @@

import { UserConfig } from '@vocab/types';
import { type UserConfig } from '@vocab/core';
interface PushOptions {

@@ -3,0 +3,0 @@ branch: string;

export * from "./declarations/src/index";
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidm9jYWItcGhyYXNlLmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi9kZWNsYXJhdGlvbnMvc3JjL2luZGV4LmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEifQ==

@@ -8,5 +8,3 @@ 'use strict';

var core = require('@vocab/core');
var FormData = require('form-data');
var fetch = require('node-fetch');
var chalk = require('chalk');
var pc = require('picocolors');
var debug = require('debug');

@@ -18,5 +16,3 @@ var sync = require('csv-stringify/sync');

var path__default = /*#__PURE__*/_interopDefault(path);
var FormData__default = /*#__PURE__*/_interopDefault(FormData);
var fetch__default = /*#__PURE__*/_interopDefault(fetch);
var chalk__default = /*#__PURE__*/_interopDefault(chalk);
var pc__default = /*#__PURE__*/_interopDefault(pc);
var debug__default = /*#__PURE__*/_interopDefault(debug);

@@ -27,6 +23,6 @@

const trace = debug__default['default'](`vocab:phrase`);
const trace = debug__default["default"](`vocab:phrase`);
const log = (...params) => {
// eslint-disable-next-line no-console
console.log(chalk__default['default'].yellow('Vocab'), ...params);
console.log(pc__default["default"].yellow('Vocab'), ...params);
};

@@ -38,3 +34,4 @@

const devLanguageTranslations = translations[devLanguage];
const csv = Object.entries(devLanguageTranslations).map(([key, {
const csvFilesByLanguage = Object.fromEntries(languages.map(language => [language, []]));
Object.entries(devLanguageTranslations).map(([key, {
message,

@@ -44,24 +41,32 @@ description,

}]) => {
const altTranslationMessages = altLanguages.map(language => {
var _translations$languag, _translations$languag2;
return (_translations$languag = translations[language]) === null || _translations$languag === void 0 ? void 0 : (_translations$languag2 = _translations$languag[key]) === null || _translations$languag2 === void 0 ? void 0 : _translations$languag2.message;
const sharedData = [key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
const devLanguageRow = [...sharedData, message];
csvFilesByLanguage[devLanguage].push(devLanguageRow);
altLanguages.map(language => {
var _translations$languag;
const altTranslationMessage = (_translations$languag = translations[language]) === null || _translations$languag === void 0 || (_translations$languag = _translations$languag[key]) === null || _translations$languag === void 0 ? void 0 : _translations$languag.message;
if (altTranslationMessage) {
csvFilesByLanguage[language].push([...sharedData, altTranslationMessage]);
}
});
return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
}); // Not spreading `languages` to ensure correct ordering of dev language first
// then alt languages
});
const csvFileStrings = Object.fromEntries(Object.entries(csvFilesByLanguage)
// Ensure CSV files are only created if the language has at least 1 translation
.filter(([_, csvFile]) => csvFile.length > 0).map(([language, csvFile]) => {
const csvFileString = sync.stringify(csvFile, {
delimiter: ',',
header: false
});
return [language, csvFileString];
}));
const csvString = sync.stringify(csv, {
delimiter: ',',
header: false
}); // Column indices start at 1
const localeMapping = Object.fromEntries(languages.map((language, index) => [language, index + 1]));
const keyIndex = languages.length + 1;
// Column indices start at 1
const keyIndex = 1;
const commentIndex = keyIndex + 1;
const tagColumn = commentIndex + 1;
const messageIndex = tagColumn + 1;
return {
csvString,
localeMapping,
csvFileStrings,
keyIndex,
messageIndex,
commentIndex,

@@ -73,15 +78,13 @@ tagColumn

/* eslint-disable no-console */
function _callPhrase(path, options = {}) {
const phraseApiToken = process.env.PHRASE_API_TOKEN;
if (!phraseApiToken) {
throw new Error('Missing PHRASE_API_TOKEN');
}
return fetch__default['default'](path, { ...options,
return fetch(path, {
...options,
headers: {
Authorization: `token ${phraseApiToken}`,
// Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
'User-Agent': 'SEEK Demo Candidate App (jhope@seek.com.au)',
'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',
...options.headers

@@ -91,4 +94,6 @@ }

console.log(`${path}: ${response.status} - ${response.statusText}`);
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${response.headers.get('X-Rate-Limit-Reset')} seconds remaining})`);
trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
const secondsUntilLimitReset = Math.ceil(Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') - Date.now() / 1000);
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${secondsUntilLimitReset} seconds remaining)`);
trace('\nLink:', response.headers.get('Link'), '\n');
// Print All Headers:
// console.log(Array.from(r.headers.entries()));

@@ -98,15 +103,10 @@

var _response$headers$get;
const result = await response.json();
trace(`Internal Result (Length: ${result.length})\n`);
if ((!options.method || options.method === 'GET') && (_response$headers$get = response.headers.get('Link')) !== null && _response$headers$get !== void 0 && _response$headers$get.includes('rel=next')) {
var _response$headers$get2, _response$headers$get3;
const [, nextPageUrl] = (_response$headers$get2 = (_response$headers$get3 = response.headers.get('Link')) === null || _response$headers$get3 === void 0 ? void 0 : _response$headers$get3.match(/<([^>]*)>; rel=next/)) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : [];
if (!nextPageUrl) {
throw new Error("Can't parse next page URL");
}
console.log('Results received with next page: ', nextPageUrl);

@@ -116,3 +116,2 @@ const nextPageResult = await _callPhrase(nextPageUrl, options);

}
return result;

@@ -125,10 +124,7 @@ } catch (e) {

}
async function callPhrase(relativePath, options = {}) {
const projectId = process.env.PHRASE_PROJECT_ID;
if (!projectId) {
throw new Error('Missing PHRASE_PROJECT_ID');
}
return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {

@@ -138,3 +134,2 @@ if (Array.isArray(result)) {

}
return result;

@@ -149,13 +144,10 @@ }).catch(error => {

const translations = {};
for (const r of phraseResult) {
if (!translations[r.locale.code]) {
translations[r.locale.code] = {};
if (!translations[r.locale.name]) {
translations[r.locale.name] = {};
}
translations[r.locale.code][r.key.name] = {
translations[r.locale.name][r.key.name] = {
message: r.content
};
}
return translations;

@@ -167,48 +159,44 @@ }

}) {
const formData = new FormData__default['default']();
const {
csvString,
localeMapping,
csvFileStrings,
keyIndex,
commentIndex,
tagColumn
tagColumn,
messageIndex
} = translationsToCsv(translationsByLanguage, devLanguage);
const fileContents = Buffer.from(csvString);
formData.append('file', fileContents, {
contentType: 'text/csv',
filename: `translations.csv`
});
formData.append('file_format', 'csv');
formData.append('branch', branch);
formData.append('update_translations', 'true');
for (const [locale, index] of Object.entries(localeMapping)) {
formData.append(`locale_mapping[${locale}]`, index);
let devLanguageUploadId = '';
for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
const formData = new FormData();
formData.append('file', new Blob([csvFileString], {
type: 'text/csv'
}), `${language}.translations.csv`);
formData.append('file_format', 'csv');
formData.append('branch', branch);
formData.append('update_translations', 'true');
formData.append('update_descriptions', 'true');
formData.append(`locale_mapping[${language}]`, messageIndex.toString());
formData.append('format_options[key_index]', keyIndex.toString());
formData.append('format_options[comment_index]', commentIndex.toString());
formData.append('format_options[tag_column]', tagColumn.toString());
formData.append('format_options[enable_pluralization]', 'false');
log(`Uploading translations for language ${language}`);
const result = await callPhrase(`uploads`, {
method: 'POST',
body: formData
});
trace('Upload result:\n', result);
if (result && 'id' in result) {
log('Upload ID:', result.id, '\n');
log('Successfully Uploaded\n');
} else {
log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
log('Response:', result);
throw new Error('Error uploading');
}
if (language === devLanguage) {
devLanguageUploadId = result.id;
}
}
formData.append('format_options[key_index]', keyIndex);
formData.append('format_options[comment_index]', commentIndex);
formData.append('format_options[tag_column]', tagColumn);
formData.append('format_options[enable_pluralization]', 'false');
log('Uploading translations');
const res = await callPhrase(`uploads`, {
method: 'POST',
body: formData
});
trace('Upload result:\n', res); // TODO: Figure out error handling
const {
id
} = res;
if (id) {
log('Upload ID:', id, '\n');
log('Successfully Uploaded\n');
} else {
log('Error uploading');
throw new Error('Error uploading');
}
return {
uploadId: id
devLanguageUploadId
};

@@ -246,3 +234,4 @@ }

async function pull({
branch = 'local-development'
branch = 'local-development',
errorOnNoGlobalKeyTranslation
}, config) {

@@ -256,7 +245,5 @@ trace(`Pulling translations from branch ${branch}`);

trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
if (!phraseLanguages.includes(config.devLanguage)) {
throw new Error(`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.`);
}
const allVocabTranslations = await core.loadAllTranslations({

@@ -267,47 +254,48 @@ fallbacks: 'none',

}, config);
for (const loadedTranslation of allVocabTranslations) {
const devTranslations = loadedTranslation.languages[config.devLanguage];
if (!devTranslations) {
throw new Error('No dev language translations loaded');
}
const defaultValues = { ...devTranslations
const defaultValues = {
...devTranslations
};
const localKeys = Object.keys(defaultValues);
for (const key of localKeys) {
defaultValues[key] = { ...defaultValues[key],
...allPhraseTranslations[config.devLanguage][core.getUniqueKey(key, loadedTranslation.namespace)]
var _defaultValues$key$gl;
defaultValues[key] = {
...defaultValues[key],
...allPhraseTranslations[config.devLanguage][(_defaultValues$key$gl = defaultValues[key].globalKey) !== null && _defaultValues$key$gl !== void 0 ? _defaultValues$key$gl : core.getUniqueKey(key, loadedTranslation.namespace)]
};
}
defaultValues._meta = loadedTranslation.metadata;
// Only write a `_meta` field if necessary
if (Object.keys(loadedTranslation.metadata).length > 0) {
defaultValues._meta = loadedTranslation.metadata;
}
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
for (const alternativeLanguage of alternativeLanguages) {
if (alternativeLanguage in allPhraseTranslations) {
const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
const altTranslations = {
...loadedTranslation.languages[alternativeLanguage]
};
const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
for (const key of localKeys) {
var _phraseAltTranslation;
const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
var _defaultValues$key$gl2, _phraseAltTranslation;
const phraseKey = (_defaultValues$key$gl2 = defaultValues[key].globalKey) !== null && _defaultValues$key$gl2 !== void 0 ? _defaultValues$key$gl2 : core.getUniqueKey(key, loadedTranslation.namespace);
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
if (!phraseTranslationMessage) {
trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {
throw new Error(`Missing translation for global key ${key} in language ${alternativeLanguage}`);
}
continue;
}
altTranslations[key] = { ...altTranslations[key],
altTranslations[key] = {
...altTranslations[key],
message: phraseTranslationMessage
};
}
const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
await mkdir(path__default['default'].dirname(altTranslationFilePath), {
await mkdir(path__default["default"].dirname(altTranslationFilePath), {
recursive: true

@@ -339,15 +327,11 @@ });

const phraseTranslations = {};
for (const loadedTranslation of allLanguageTranslations) {
for (const language of allLanguages) {
const localTranslations = loadedTranslation.languages[language];
if (!localTranslations) {
continue;
}
if (!phraseTranslations[language]) {
phraseTranslations[language] = {};
}
const {

@@ -358,5 +342,3 @@ metadata: {

} = loadedTranslation;
for (const localKey of Object.keys(localTranslations)) {
const phraseKey = core.getUniqueKey(localKey, loadedTranslation.namespace);
const {

@@ -366,7 +348,7 @@ tags = [],

} = localTranslations[localKey];
if (language === config.devLanguage) {
localTranslation.tags = [...tags, ...sharedTags];
}
const globalKey = loadedTranslation.languages[config.devLanguage][localKey].globalKey;
const phraseKey = globalKey !== null && globalKey !== void 0 ? globalKey : core.getUniqueKey(localKey, loadedTranslation.namespace);
phraseTranslations[language][phraseKey] = localTranslation;

@@ -376,5 +358,4 @@ }

}
const {
uploadId
devLanguageUploadId
} = await pushTranslations(phraseTranslations, {

@@ -384,5 +365,4 @@ devLanguage: config.devLanguage,

});
if (deleteUnusedKeys$1) {
await deleteUnusedKeys(uploadId, branch);
await deleteUnusedKeys(devLanguageUploadId, branch);
}

@@ -389,0 +369,0 @@ }

@@ -8,5 +8,3 @@ 'use strict';

var core = require('@vocab/core');
var FormData = require('form-data');
var fetch = require('node-fetch');
var chalk = require('chalk');
var pc = require('picocolors');
var debug = require('debug');

@@ -18,5 +16,3 @@ var sync = require('csv-stringify/sync');

var path__default = /*#__PURE__*/_interopDefault(path);
var FormData__default = /*#__PURE__*/_interopDefault(FormData);
var fetch__default = /*#__PURE__*/_interopDefault(fetch);
var chalk__default = /*#__PURE__*/_interopDefault(chalk);
var pc__default = /*#__PURE__*/_interopDefault(pc);
var debug__default = /*#__PURE__*/_interopDefault(debug);

@@ -27,6 +23,6 @@

const trace = debug__default['default'](`vocab:phrase`);
const trace = debug__default["default"](`vocab:phrase`);
const log = (...params) => {
// eslint-disable-next-line no-console
console.log(chalk__default['default'].yellow('Vocab'), ...params);
console.log(pc__default["default"].yellow('Vocab'), ...params);
};

@@ -38,3 +34,4 @@

const devLanguageTranslations = translations[devLanguage];
const csv = Object.entries(devLanguageTranslations).map(([key, {
const csvFilesByLanguage = Object.fromEntries(languages.map(language => [language, []]));
Object.entries(devLanguageTranslations).map(([key, {
message,

@@ -44,24 +41,32 @@ description,

}]) => {
const altTranslationMessages = altLanguages.map(language => {
var _translations$languag, _translations$languag2;
return (_translations$languag = translations[language]) === null || _translations$languag === void 0 ? void 0 : (_translations$languag2 = _translations$languag[key]) === null || _translations$languag2 === void 0 ? void 0 : _translations$languag2.message;
const sharedData = [key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
const devLanguageRow = [...sharedData, message];
csvFilesByLanguage[devLanguage].push(devLanguageRow);
altLanguages.map(language => {
var _translations$languag;
const altTranslationMessage = (_translations$languag = translations[language]) === null || _translations$languag === void 0 || (_translations$languag = _translations$languag[key]) === null || _translations$languag === void 0 ? void 0 : _translations$languag.message;
if (altTranslationMessage) {
csvFilesByLanguage[language].push([...sharedData, altTranslationMessage]);
}
});
return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
}); // Not spreading `languages` to ensure correct ordering of dev language first
// then alt languages
});
const csvFileStrings = Object.fromEntries(Object.entries(csvFilesByLanguage)
// Ensure CSV files are only created if the language has at least 1 translation
.filter(([_, csvFile]) => csvFile.length > 0).map(([language, csvFile]) => {
const csvFileString = sync.stringify(csvFile, {
delimiter: ',',
header: false
});
return [language, csvFileString];
}));
const csvString = sync.stringify(csv, {
delimiter: ',',
header: false
}); // Column indices start at 1
const localeMapping = Object.fromEntries(languages.map((language, index) => [language, index + 1]));
const keyIndex = languages.length + 1;
// Column indices start at 1
const keyIndex = 1;
const commentIndex = keyIndex + 1;
const tagColumn = commentIndex + 1;
const messageIndex = tagColumn + 1;
return {
csvString,
localeMapping,
csvFileStrings,
keyIndex,
messageIndex,
commentIndex,

@@ -73,15 +78,13 @@ tagColumn

/* eslint-disable no-console */
function _callPhrase(path, options = {}) {
const phraseApiToken = process.env.PHRASE_API_TOKEN;
if (!phraseApiToken) {
throw new Error('Missing PHRASE_API_TOKEN');
}
return fetch__default['default'](path, { ...options,
return fetch(path, {
...options,
headers: {
Authorization: `token ${phraseApiToken}`,
// Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
'User-Agent': 'SEEK Demo Candidate App (jhope@seek.com.au)',
'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',
...options.headers

@@ -91,4 +94,6 @@ }

console.log(`${path}: ${response.status} - ${response.statusText}`);
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${response.headers.get('X-Rate-Limit-Reset')} seconds remaining})`);
trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
const secondsUntilLimitReset = Math.ceil(Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') - Date.now() / 1000);
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${secondsUntilLimitReset} seconds remaining)`);
trace('\nLink:', response.headers.get('Link'), '\n');
// Print All Headers:
// console.log(Array.from(r.headers.entries()));

@@ -98,15 +103,10 @@

var _response$headers$get;
const result = await response.json();
trace(`Internal Result (Length: ${result.length})\n`);
if ((!options.method || options.method === 'GET') && (_response$headers$get = response.headers.get('Link')) !== null && _response$headers$get !== void 0 && _response$headers$get.includes('rel=next')) {
var _response$headers$get2, _response$headers$get3;
const [, nextPageUrl] = (_response$headers$get2 = (_response$headers$get3 = response.headers.get('Link')) === null || _response$headers$get3 === void 0 ? void 0 : _response$headers$get3.match(/<([^>]*)>; rel=next/)) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : [];
if (!nextPageUrl) {
throw new Error("Can't parse next page URL");
}
console.log('Results received with next page: ', nextPageUrl);

@@ -116,3 +116,2 @@ const nextPageResult = await _callPhrase(nextPageUrl, options);

}
return result;

@@ -125,10 +124,7 @@ } catch (e) {

}
async function callPhrase(relativePath, options = {}) {
const projectId = process.env.PHRASE_PROJECT_ID;
if (!projectId) {
throw new Error('Missing PHRASE_PROJECT_ID');
}
return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {

@@ -138,3 +134,2 @@ if (Array.isArray(result)) {

}
return result;

@@ -149,13 +144,10 @@ }).catch(error => {

const translations = {};
for (const r of phraseResult) {
if (!translations[r.locale.code]) {
translations[r.locale.code] = {};
if (!translations[r.locale.name]) {
translations[r.locale.name] = {};
}
translations[r.locale.code][r.key.name] = {
translations[r.locale.name][r.key.name] = {
message: r.content
};
}
return translations;

@@ -167,48 +159,44 @@ }

}) {
const formData = new FormData__default['default']();
const {
csvString,
localeMapping,
csvFileStrings,
keyIndex,
commentIndex,
tagColumn
tagColumn,
messageIndex
} = translationsToCsv(translationsByLanguage, devLanguage);
const fileContents = Buffer.from(csvString);
formData.append('file', fileContents, {
contentType: 'text/csv',
filename: `translations.csv`
});
formData.append('file_format', 'csv');
formData.append('branch', branch);
formData.append('update_translations', 'true');
for (const [locale, index] of Object.entries(localeMapping)) {
formData.append(`locale_mapping[${locale}]`, index);
let devLanguageUploadId = '';
for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
const formData = new FormData();
formData.append('file', new Blob([csvFileString], {
type: 'text/csv'
}), `${language}.translations.csv`);
formData.append('file_format', 'csv');
formData.append('branch', branch);
formData.append('update_translations', 'true');
formData.append('update_descriptions', 'true');
formData.append(`locale_mapping[${language}]`, messageIndex.toString());
formData.append('format_options[key_index]', keyIndex.toString());
formData.append('format_options[comment_index]', commentIndex.toString());
formData.append('format_options[tag_column]', tagColumn.toString());
formData.append('format_options[enable_pluralization]', 'false');
log(`Uploading translations for language ${language}`);
const result = await callPhrase(`uploads`, {
method: 'POST',
body: formData
});
trace('Upload result:\n', result);
if (result && 'id' in result) {
log('Upload ID:', result.id, '\n');
log('Successfully Uploaded\n');
} else {
log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
log('Response:', result);
throw new Error('Error uploading');
}
if (language === devLanguage) {
devLanguageUploadId = result.id;
}
}
formData.append('format_options[key_index]', keyIndex);
formData.append('format_options[comment_index]', commentIndex);
formData.append('format_options[tag_column]', tagColumn);
formData.append('format_options[enable_pluralization]', 'false');
log('Uploading translations');
const res = await callPhrase(`uploads`, {
method: 'POST',
body: formData
});
trace('Upload result:\n', res); // TODO: Figure out error handling
const {
id
} = res;
if (id) {
log('Upload ID:', id, '\n');
log('Successfully Uploaded\n');
} else {
log('Error uploading');
throw new Error('Error uploading');
}
return {
uploadId: id
devLanguageUploadId
};

@@ -246,3 +234,4 @@ }

async function pull({
branch = 'local-development'
branch = 'local-development',
errorOnNoGlobalKeyTranslation
}, config) {

@@ -256,7 +245,5 @@ trace(`Pulling translations from branch ${branch}`);

trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
if (!phraseLanguages.includes(config.devLanguage)) {
throw new Error(`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.`);
}
const allVocabTranslations = await core.loadAllTranslations({

@@ -267,47 +254,48 @@ fallbacks: 'none',

}, config);
for (const loadedTranslation of allVocabTranslations) {
const devTranslations = loadedTranslation.languages[config.devLanguage];
if (!devTranslations) {
throw new Error('No dev language translations loaded');
}
const defaultValues = { ...devTranslations
const defaultValues = {
...devTranslations
};
const localKeys = Object.keys(defaultValues);
for (const key of localKeys) {
defaultValues[key] = { ...defaultValues[key],
...allPhraseTranslations[config.devLanguage][core.getUniqueKey(key, loadedTranslation.namespace)]
var _defaultValues$key$gl;
defaultValues[key] = {
...defaultValues[key],
...allPhraseTranslations[config.devLanguage][(_defaultValues$key$gl = defaultValues[key].globalKey) !== null && _defaultValues$key$gl !== void 0 ? _defaultValues$key$gl : core.getUniqueKey(key, loadedTranslation.namespace)]
};
}
defaultValues._meta = loadedTranslation.metadata;
// Only write a `_meta` field if necessary
if (Object.keys(loadedTranslation.metadata).length > 0) {
defaultValues._meta = loadedTranslation.metadata;
}
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
for (const alternativeLanguage of alternativeLanguages) {
if (alternativeLanguage in allPhraseTranslations) {
const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
const altTranslations = {
...loadedTranslation.languages[alternativeLanguage]
};
const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
for (const key of localKeys) {
var _phraseAltTranslation;
const phraseKey = core.getUniqueKey(key, loadedTranslation.namespace);
var _defaultValues$key$gl2, _phraseAltTranslation;
const phraseKey = (_defaultValues$key$gl2 = defaultValues[key].globalKey) !== null && _defaultValues$key$gl2 !== void 0 ? _defaultValues$key$gl2 : core.getUniqueKey(key, loadedTranslation.namespace);
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
if (!phraseTranslationMessage) {
trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {
throw new Error(`Missing translation for global key ${key} in language ${alternativeLanguage}`);
}
continue;
}
altTranslations[key] = { ...altTranslations[key],
altTranslations[key] = {
...altTranslations[key],
message: phraseTranslationMessage
};
}
const altTranslationFilePath = core.getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);
await mkdir(path__default['default'].dirname(altTranslationFilePath), {
await mkdir(path__default["default"].dirname(altTranslationFilePath), {
recursive: true

@@ -339,15 +327,11 @@ });

const phraseTranslations = {};
for (const loadedTranslation of allLanguageTranslations) {
for (const language of allLanguages) {
const localTranslations = loadedTranslation.languages[language];
if (!localTranslations) {
continue;
}
if (!phraseTranslations[language]) {
phraseTranslations[language] = {};
}
const {

@@ -358,5 +342,3 @@ metadata: {

} = loadedTranslation;
for (const localKey of Object.keys(localTranslations)) {
const phraseKey = core.getUniqueKey(localKey, loadedTranslation.namespace);
const {

@@ -366,7 +348,7 @@ tags = [],

} = localTranslations[localKey];
if (language === config.devLanguage) {
localTranslation.tags = [...tags, ...sharedTags];
}
const globalKey = loadedTranslation.languages[config.devLanguage][localKey].globalKey;
const phraseKey = globalKey !== null && globalKey !== void 0 ? globalKey : core.getUniqueKey(localKey, loadedTranslation.namespace);
phraseTranslations[language][phraseKey] = localTranslation;

@@ -376,5 +358,4 @@ }

}
const {
uploadId
devLanguageUploadId
} = await pushTranslations(phraseTranslations, {

@@ -384,5 +365,4 @@ devLanguage: config.devLanguage,

});
if (deleteUnusedKeys$1) {
await deleteUnusedKeys(uploadId, branch);
await deleteUnusedKeys(devLanguageUploadId, branch);
}

@@ -389,0 +369,0 @@ }

import { promises } from 'fs';
import path from 'path';
import { getAltLanguages, loadAllTranslations, getUniqueKey, getAltLanguageFilePath } from '@vocab/core';
import FormData from 'form-data';
import fetch from 'node-fetch';
import chalk from 'chalk';
import pc from 'picocolors';
import debug from 'debug';

@@ -16,3 +14,3 @@ import { stringify } from 'csv-stringify/sync';

// eslint-disable-next-line no-console
console.log(chalk.yellow('Vocab'), ...params);
console.log(pc.yellow('Vocab'), ...params);
};

@@ -24,3 +22,4 @@

const devLanguageTranslations = translations[devLanguage];
const csv = Object.entries(devLanguageTranslations).map(([key, {
const csvFilesByLanguage = Object.fromEntries(languages.map(language => [language, []]));
Object.entries(devLanguageTranslations).map(([key, {
message,

@@ -30,24 +29,32 @@ description,

}]) => {
const altTranslationMessages = altLanguages.map(language => {
var _translations$languag, _translations$languag2;
return (_translations$languag = translations[language]) === null || _translations$languag === void 0 ? void 0 : (_translations$languag2 = _translations$languag[key]) === null || _translations$languag2 === void 0 ? void 0 : _translations$languag2.message;
const sharedData = [key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
const devLanguageRow = [...sharedData, message];
csvFilesByLanguage[devLanguage].push(devLanguageRow);
altLanguages.map(language => {
var _translations$languag;
const altTranslationMessage = (_translations$languag = translations[language]) === null || _translations$languag === void 0 || (_translations$languag = _translations$languag[key]) === null || _translations$languag === void 0 ? void 0 : _translations$languag.message;
if (altTranslationMessage) {
csvFilesByLanguage[language].push([...sharedData, altTranslationMessage]);
}
});
return [message, ...altTranslationMessages, key, description, tags === null || tags === void 0 ? void 0 : tags.join(',')];
}); // Not spreading `languages` to ensure correct ordering of dev language first
// then alt languages
});
const csvFileStrings = Object.fromEntries(Object.entries(csvFilesByLanguage)
// Ensure CSV files are only created if the language has at least 1 translation
.filter(([_, csvFile]) => csvFile.length > 0).map(([language, csvFile]) => {
const csvFileString = stringify(csvFile, {
delimiter: ',',
header: false
});
return [language, csvFileString];
}));
const csvString = stringify(csv, {
delimiter: ',',
header: false
}); // Column indices start at 1
const localeMapping = Object.fromEntries(languages.map((language, index) => [language, index + 1]));
const keyIndex = languages.length + 1;
// Column indices start at 1
const keyIndex = 1;
const commentIndex = keyIndex + 1;
const tagColumn = commentIndex + 1;
const messageIndex = tagColumn + 1;
return {
csvString,
localeMapping,
csvFileStrings,
keyIndex,
messageIndex,
commentIndex,

@@ -59,15 +66,13 @@ tagColumn

/* eslint-disable no-console */
function _callPhrase(path, options = {}) {
const phraseApiToken = process.env.PHRASE_API_TOKEN;
if (!phraseApiToken) {
throw new Error('Missing PHRASE_API_TOKEN');
}
return fetch(path, { ...options,
return fetch(path, {
...options,
headers: {
Authorization: `token ${phraseApiToken}`,
// Provide identification via User Agent as requested in https://developers.phrase.com/api/#overview--identification-via-user-agent
'User-Agent': 'SEEK Demo Candidate App (jhope@seek.com.au)',
'User-Agent': 'Vocab Client (https://github.com/seek-oss/vocab)',
...options.headers

@@ -77,4 +82,6 @@ }

console.log(`${path}: ${response.status} - ${response.statusText}`);
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${response.headers.get('X-Rate-Limit-Reset')} seconds remaining})`);
trace('\nLink:', response.headers.get('Link'), '\n'); // Print All Headers:
const secondsUntilLimitReset = Math.ceil(Number.parseFloat(response.headers.get('X-Rate-Limit-Reset') || '0') - Date.now() / 1000);
console.log(`Rate Limit: ${response.headers.get('X-Rate-Limit-Remaining')} of ${response.headers.get('X-Rate-Limit-Limit')} remaining. (${secondsUntilLimitReset} seconds remaining)`);
trace('\nLink:', response.headers.get('Link'), '\n');
// Print All Headers:
// console.log(Array.from(r.headers.entries()));

@@ -84,15 +91,10 @@

var _response$headers$get;
const result = await response.json();
trace(`Internal Result (Length: ${result.length})\n`);
if ((!options.method || options.method === 'GET') && (_response$headers$get = response.headers.get('Link')) !== null && _response$headers$get !== void 0 && _response$headers$get.includes('rel=next')) {
var _response$headers$get2, _response$headers$get3;
const [, nextPageUrl] = (_response$headers$get2 = (_response$headers$get3 = response.headers.get('Link')) === null || _response$headers$get3 === void 0 ? void 0 : _response$headers$get3.match(/<([^>]*)>; rel=next/)) !== null && _response$headers$get2 !== void 0 ? _response$headers$get2 : [];
if (!nextPageUrl) {
throw new Error("Can't parse next page URL");
}
console.log('Results received with next page: ', nextPageUrl);

@@ -102,3 +104,2 @@ const nextPageResult = await _callPhrase(nextPageUrl, options);

}
return result;

@@ -111,10 +112,7 @@ } catch (e) {

}
async function callPhrase(relativePath, options = {}) {
const projectId = process.env.PHRASE_PROJECT_ID;
if (!projectId) {
throw new Error('Missing PHRASE_PROJECT_ID');
}
return _callPhrase(`https://api.phrase.com/v2/projects/${projectId}/${relativePath}`, options).then(result => {

@@ -124,3 +122,2 @@ if (Array.isArray(result)) {

}
return result;

@@ -135,13 +132,10 @@ }).catch(error => {

const translations = {};
for (const r of phraseResult) {
if (!translations[r.locale.code]) {
translations[r.locale.code] = {};
if (!translations[r.locale.name]) {
translations[r.locale.name] = {};
}
translations[r.locale.code][r.key.name] = {
translations[r.locale.name][r.key.name] = {
message: r.content
};
}
return translations;

@@ -153,48 +147,44 @@ }

}) {
const formData = new FormData();
const {
csvString,
localeMapping,
csvFileStrings,
keyIndex,
commentIndex,
tagColumn
tagColumn,
messageIndex
} = translationsToCsv(translationsByLanguage, devLanguage);
const fileContents = Buffer.from(csvString);
formData.append('file', fileContents, {
contentType: 'text/csv',
filename: `translations.csv`
});
formData.append('file_format', 'csv');
formData.append('branch', branch);
formData.append('update_translations', 'true');
for (const [locale, index] of Object.entries(localeMapping)) {
formData.append(`locale_mapping[${locale}]`, index);
let devLanguageUploadId = '';
for (const [language, csvFileString] of Object.entries(csvFileStrings)) {
const formData = new FormData();
formData.append('file', new Blob([csvFileString], {
type: 'text/csv'
}), `${language}.translations.csv`);
formData.append('file_format', 'csv');
formData.append('branch', branch);
formData.append('update_translations', 'true');
formData.append('update_descriptions', 'true');
formData.append(`locale_mapping[${language}]`, messageIndex.toString());
formData.append('format_options[key_index]', keyIndex.toString());
formData.append('format_options[comment_index]', commentIndex.toString());
formData.append('format_options[tag_column]', tagColumn.toString());
formData.append('format_options[enable_pluralization]', 'false');
log(`Uploading translations for language ${language}`);
const result = await callPhrase(`uploads`, {
method: 'POST',
body: formData
});
trace('Upload result:\n', result);
if (result && 'id' in result) {
log('Upload ID:', result.id, '\n');
log('Successfully Uploaded\n');
} else {
log(`Error uploading: ${result === null || result === void 0 ? void 0 : result.message}\n`);
log('Response:', result);
throw new Error('Error uploading');
}
if (language === devLanguage) {
devLanguageUploadId = result.id;
}
}
formData.append('format_options[key_index]', keyIndex);
formData.append('format_options[comment_index]', commentIndex);
formData.append('format_options[tag_column]', tagColumn);
formData.append('format_options[enable_pluralization]', 'false');
log('Uploading translations');
const res = await callPhrase(`uploads`, {
method: 'POST',
body: formData
});
trace('Upload result:\n', res); // TODO: Figure out error handling
const {
id
} = res;
if (id) {
log('Upload ID:', id, '\n');
log('Successfully Uploaded\n');
} else {
log('Error uploading');
throw new Error('Error uploading');
}
return {
uploadId: id
devLanguageUploadId
};

@@ -232,3 +222,4 @@ }

async function pull({
branch = 'local-development'
branch = 'local-development',
errorOnNoGlobalKeyTranslation
}, config) {

@@ -242,7 +233,5 @@ trace(`Pulling translations from branch ${branch}`);

trace(`Found Phrase translations for languages ${phraseLanguages.join(', ')}`);
if (!phraseLanguages.includes(config.devLanguage)) {
throw new Error(`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.`);
}
const allVocabTranslations = await loadAllTranslations({

@@ -253,45 +242,46 @@ fallbacks: 'none',

}, config);
for (const loadedTranslation of allVocabTranslations) {
const devTranslations = loadedTranslation.languages[config.devLanguage];
if (!devTranslations) {
throw new Error('No dev language translations loaded');
}
const defaultValues = { ...devTranslations
const defaultValues = {
...devTranslations
};
const localKeys = Object.keys(defaultValues);
for (const key of localKeys) {
defaultValues[key] = { ...defaultValues[key],
...allPhraseTranslations[config.devLanguage][getUniqueKey(key, loadedTranslation.namespace)]
var _defaultValues$key$gl;
defaultValues[key] = {
...defaultValues[key],
...allPhraseTranslations[config.devLanguage][(_defaultValues$key$gl = defaultValues[key].globalKey) !== null && _defaultValues$key$gl !== void 0 ? _defaultValues$key$gl : getUniqueKey(key, loadedTranslation.namespace)]
};
}
defaultValues._meta = loadedTranslation.metadata;
// Only write a `_meta` field if necessary
if (Object.keys(loadedTranslation.metadata).length > 0) {
defaultValues._meta = loadedTranslation.metadata;
}
await writeFile(loadedTranslation.filePath, `${JSON.stringify(defaultValues, null, 2)}\n`);
for (const alternativeLanguage of alternativeLanguages) {
if (alternativeLanguage in allPhraseTranslations) {
const altTranslations = { ...loadedTranslation.languages[alternativeLanguage]
const altTranslations = {
...loadedTranslation.languages[alternativeLanguage]
};
const phraseAltTranslations = allPhraseTranslations[alternativeLanguage];
for (const key of localKeys) {
var _phraseAltTranslation;
const phraseKey = getUniqueKey(key, loadedTranslation.namespace);
var _defaultValues$key$gl2, _phraseAltTranslation;
const phraseKey = (_defaultValues$key$gl2 = defaultValues[key].globalKey) !== null && _defaultValues$key$gl2 !== void 0 ? _defaultValues$key$gl2 : getUniqueKey(key, loadedTranslation.namespace);
const phraseTranslationMessage = (_phraseAltTranslation = phraseAltTranslations[phraseKey]) === null || _phraseAltTranslation === void 0 ? void 0 : _phraseAltTranslation.message;
if (!phraseTranslationMessage) {
trace(`Missing translation. No translation for key ${key} in phrase as ${phraseKey} in language ${alternativeLanguage}.`);
if (errorOnNoGlobalKeyTranslation && defaultValues[key].globalKey) {
throw new Error(`Missing translation for global key ${key} in language ${alternativeLanguage}`);
}
continue;
}
altTranslations[key] = { ...altTranslations[key],
altTranslations[key] = {
...altTranslations[key],
message: phraseTranslationMessage
};
}
const altTranslationFilePath = getAltLanguageFilePath(loadedTranslation.filePath, alternativeLanguage);

@@ -325,15 +315,11 @@ await mkdir(path.dirname(altTranslationFilePath), {

const phraseTranslations = {};
for (const loadedTranslation of allLanguageTranslations) {
for (const language of allLanguages) {
const localTranslations = loadedTranslation.languages[language];
if (!localTranslations) {
continue;
}
if (!phraseTranslations[language]) {
phraseTranslations[language] = {};
}
const {

@@ -344,5 +330,3 @@ metadata: {

} = loadedTranslation;
for (const localKey of Object.keys(localTranslations)) {
const phraseKey = getUniqueKey(localKey, loadedTranslation.namespace);
const {

@@ -352,7 +336,7 @@ tags = [],

} = localTranslations[localKey];
if (language === config.devLanguage) {
localTranslation.tags = [...tags, ...sharedTags];
}
const globalKey = loadedTranslation.languages[config.devLanguage][localKey].globalKey;
const phraseKey = globalKey !== null && globalKey !== void 0 ? globalKey : getUniqueKey(localKey, loadedTranslation.namespace);
phraseTranslations[language][phraseKey] = localTranslation;

@@ -362,5 +346,4 @@ }

}
const {
uploadId
devLanguageUploadId
} = await pushTranslations(phraseTranslations, {

@@ -370,5 +353,4 @@ devLanguage: config.devLanguage,

});
if (deleteUnusedKeys$1) {
await deleteUnusedKeys(uploadId, branch);
await deleteUnusedKeys(devLanguageUploadId, branch);
}

@@ -375,0 +357,0 @@ }

{
"name": "@vocab/phrase",
"version": "0.0.0-tags-support-2023185232",
"version": "0.0.0-use-phrase-locale-name-20240924060647",
"repository": {
"type": "git",
"url": "https://github.com/seek-oss/vocab.git",
"directory": "packages/phrase"
},
"engines": {
"node": ">=18"
},
"main": "dist/vocab-phrase.cjs.js",

@@ -9,12 +17,10 @@ "module": "dist/vocab-phrase.esm.js",

"dependencies": {
"@vocab/core": "^0.0.0-tags-support-2023185232",
"@vocab/types": "^0.0.0-tags-support-2023185232",
"chalk": "^4.1.0",
"csv-stringify": "^6.2.3",
"debug": "^4.3.1",
"form-data": "^3.0.0",
"node-fetch": "^2.6.1"
"picocolors": "^1.0.0",
"@vocab/core": "^1.6.2"
},
"devDependencies": {
"@types/node-fetch": "^2.5.7"
"@types/debug": "^4.1.5",
"@types/node": "^18.11.9"
},

@@ -24,2 +30,2 @@ "files": [

]
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc