lingo.dev 0.113.3 → 0.113.4

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/build/cli.mjs CHANGED
@@ -1874,9 +1874,9 @@ function createJsoncLoader() {
1874
1874
  import { flatten, unflatten } from "flat";
1875
1875
  import _6 from "lodash";
1876
1876
  var OBJECT_NUMERIC_KEY_PREFIX = "__lingodotdev__obj__";
1877
- function createFlatLoader() {
1877
+ function createFlatLoader(options) {
1878
1878
  const composedLoader = composeLoaders(
1879
- createDenormalizeLoader(),
1879
+ createDenormalizeLoader(options),
1880
1880
  createNormalizeLoader()
1881
1881
  );
1882
1882
  return {
@@ -1889,16 +1889,29 @@ function createFlatLoader() {
1889
1889
  }
1890
1890
  };
1891
1891
  }
1892
- function createDenormalizeLoader() {
1892
+ function createDenormalizeLoader(options) {
1893
1893
  return createLoader({
1894
1894
  pull: async (locale, input2) => {
1895
1895
  const inputDenormalized = denormalizeObjectKeys(input2 || {});
1896
- const denormalized = flatten(inputDenormalized, {
1896
+ const preservedObjects = {};
1897
+ const nonPreservedInput = {};
1898
+ for (const [key, value] of Object.entries(inputDenormalized)) {
1899
+ if (options?.shouldPreserveObject?.(value)) {
1900
+ preservedObjects[key] = value;
1901
+ } else {
1902
+ nonPreservedInput[key] = value;
1903
+ }
1904
+ }
1905
+ const flattened = flatten(nonPreservedInput, {
1897
1906
  delimiter: "/",
1898
1907
  transformKey(key) {
1899
1908
  return encodeURIComponent(String(key));
1900
1909
  }
1901
1910
  });
1911
+ const denormalized = {
1912
+ ...flattened,
1913
+ ...preservedObjects
1914
+ };
1902
1915
  const keysMap = buildDenormalizedKeysMap(denormalized);
1903
1916
  return { denormalized, keysMap };
1904
1917
  },
@@ -2941,6 +2954,380 @@ function _removeLocale(input2, locale) {
2941
2954
  return { ...input2, strings: newStrings };
2942
2955
  }
2943
2956
 
2957
+ // src/cli/loaders/xcode-xcstrings-icu.ts
2958
+ var ICU_TYPE_MARKER = Symbol.for("@lingo.dev/icu-plural-object");
2959
+ var CLDR_PLURAL_CATEGORIES = /* @__PURE__ */ new Set([
2960
+ "zero",
2961
+ "one",
2962
+ "two",
2963
+ "few",
2964
+ "many",
2965
+ "other"
2966
+ ]);
2967
+ function isICUPluralObject(value) {
2968
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2969
+ return false;
2970
+ }
2971
+ if (ICU_TYPE_MARKER in value) {
2972
+ return true;
2973
+ }
2974
+ if (!("icu" in value) || typeof value.icu !== "string") {
2975
+ return false;
2976
+ }
2977
+ const icuPluralPattern = /^\{[\w]+,\s*plural,\s*.+\}$/;
2978
+ if (!icuPluralPattern.test(value.icu)) {
2979
+ return false;
2980
+ }
2981
+ if (value._meta !== void 0) {
2982
+ if (typeof value._meta !== "object" || !value._meta.variables || typeof value._meta.variables !== "object") {
2983
+ return false;
2984
+ }
2985
+ for (const [varName, varMeta] of Object.entries(value._meta.variables)) {
2986
+ if (!varMeta || typeof varMeta !== "object" || typeof varMeta.format !== "string" || varMeta.role !== "plural" && varMeta.role !== "other") {
2987
+ return false;
2988
+ }
2989
+ }
2990
+ }
2991
+ return true;
2992
+ }
2993
+ function isPluralFormsObject(value) {
2994
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2995
+ return false;
2996
+ }
2997
+ const keys = Object.keys(value);
2998
+ if (keys.length === 0) {
2999
+ return false;
3000
+ }
3001
+ const allKeysAreCldr = keys.every((key) => CLDR_PLURAL_CATEGORIES.has(key));
3002
+ if (!allKeysAreCldr) {
3003
+ return false;
3004
+ }
3005
+ const allValuesAreStrings = keys.every(
3006
+ (key) => typeof value[key] === "string"
3007
+ );
3008
+ if (!allValuesAreStrings) {
3009
+ return false;
3010
+ }
3011
+ if (!("other" in value)) {
3012
+ return false;
3013
+ }
3014
+ return true;
3015
+ }
3016
+ function getRequiredPluralCategories(locale) {
3017
+ try {
3018
+ const pluralRules = new Intl.PluralRules(locale);
3019
+ const categories = pluralRules.resolvedOptions().pluralCategories;
3020
+ if (!categories || categories.length === 0) {
3021
+ throw new Error(`No plural categories found for locale: ${locale}`);
3022
+ }
3023
+ return categories;
3024
+ } catch (error) {
3025
+ console.warn(
3026
+ `[xcode-xcstrings-icu] Failed to resolve plural categories for locale "${locale}". Using fallback ["one", "other"]. Error: ${error instanceof Error ? error.message : String(error)}`
3027
+ );
3028
+ return ["one", "other"];
3029
+ }
3030
+ }
3031
+ var CLDR_CATEGORY_TO_NUMBER = {
3032
+ zero: 0,
3033
+ one: 1,
3034
+ two: 2
3035
+ };
3036
+ var NUMBER_TO_CLDR_CATEGORY = {
3037
+ 0: "zero",
3038
+ 1: "one",
3039
+ 2: "two"
3040
+ };
3041
+ function xcstringsToPluralWithMeta(pluralForms, sourceLocale = "en") {
3042
+ if (!pluralForms || Object.keys(pluralForms).length === 0) {
3043
+ throw new Error("pluralForms cannot be empty");
3044
+ }
3045
+ const requiredCategories = getRequiredPluralCategories(sourceLocale);
3046
+ const variables = {};
3047
+ const formatRegex = /(%(?:(\d+)\$)?(?:[+-])?(?:\d+)?(?:\.(\d+))?([lhqLzjt]*)([diuoxXfFeEgGaAcspn@]))/g;
3048
+ let maxMatches = [];
3049
+ let maxMatchText = "";
3050
+ for (const [form, text] of Object.entries(pluralForms)) {
3051
+ if (typeof text !== "string") {
3052
+ console.warn(
3053
+ `Warning: Plural form "${form}" has non-string value:`,
3054
+ text
3055
+ );
3056
+ continue;
3057
+ }
3058
+ const matches = [...text.matchAll(formatRegex)];
3059
+ if (matches.length > maxMatches.length) {
3060
+ maxMatches = matches;
3061
+ maxMatchText = text;
3062
+ }
3063
+ }
3064
+ let lastNumericIndex = -1;
3065
+ maxMatches.forEach((match2, idx) => {
3066
+ const specifier = match2[5];
3067
+ if (/[diuoxXfFeE]/.test(specifier)) {
3068
+ lastNumericIndex = idx;
3069
+ }
3070
+ });
3071
+ let nonPluralCounter = 0;
3072
+ maxMatches.forEach((match2, idx) => {
3073
+ const fullFormat = match2[1];
3074
+ const position = match2[2];
3075
+ const precision = match2[3];
3076
+ const lengthMod = match2[4];
3077
+ const specifier = match2[5];
3078
+ const isPluralVar = idx === lastNumericIndex;
3079
+ const varName = isPluralVar ? "count" : `var${nonPluralCounter++}`;
3080
+ variables[varName] = {
3081
+ format: fullFormat,
3082
+ role: isPluralVar ? "plural" : "other"
3083
+ };
3084
+ });
3085
+ const variableKeys = Object.keys(variables);
3086
+ const icuForms = Object.entries(pluralForms).filter(([form, text]) => {
3087
+ if (typeof text !== "string") {
3088
+ return false;
3089
+ }
3090
+ return true;
3091
+ }).map(([form, text]) => {
3092
+ let processed = text;
3093
+ let vIdx = 0;
3094
+ processed = processed.replace(formatRegex, () => {
3095
+ if (vIdx >= variableKeys.length) {
3096
+ vIdx++;
3097
+ return "#";
3098
+ }
3099
+ const varName = variableKeys[vIdx];
3100
+ const varMeta = variables[varName];
3101
+ vIdx++;
3102
+ if (varMeta.role === "plural") {
3103
+ return "#";
3104
+ } else {
3105
+ return `{${varName}}`;
3106
+ }
3107
+ });
3108
+ const isRequired = requiredCategories.includes(form);
3109
+ const formKey = !isRequired && form in CLDR_CATEGORY_TO_NUMBER ? `=${CLDR_CATEGORY_TO_NUMBER[form]}` : form;
3110
+ return `${formKey} {${processed}}`;
3111
+ }).join(" ");
3112
+ const pluralVarName = Object.keys(variables).find((name) => variables[name].role === "plural") || "count";
3113
+ const icu = `{${pluralVarName}, plural, ${icuForms}}`;
3114
+ const result = {
3115
+ icu,
3116
+ _meta: Object.keys(variables).length > 0 ? { variables } : void 0,
3117
+ [ICU_TYPE_MARKER]: true
3118
+ // Add type marker for robust detection
3119
+ };
3120
+ return result;
3121
+ }
3122
+ function pluralWithMetaToXcstrings(data) {
3123
+ if (!data.icu) {
3124
+ throw new Error("ICU string is required");
3125
+ }
3126
+ const ast = parseICU(data.icu);
3127
+ if (!ast || ast.length === 0) {
3128
+ throw new Error("Invalid ICU format");
3129
+ }
3130
+ const pluralNode = ast.find((node) => node.type === "plural");
3131
+ if (!pluralNode) {
3132
+ throw new Error("No plural found in ICU format");
3133
+ }
3134
+ const forms = {};
3135
+ for (const [form, option] of Object.entries(pluralNode.options)) {
3136
+ let text = "";
3137
+ const optionValue = option.value;
3138
+ for (const element of optionValue) {
3139
+ if (element.type === "literal") {
3140
+ text += element.value;
3141
+ } else if (element.type === "pound") {
3142
+ const pluralVar = Object.entries(data._meta?.variables || {}).find(
3143
+ ([_36, meta]) => meta.role === "plural"
3144
+ );
3145
+ text += pluralVar?.[1].format || "%lld";
3146
+ } else if (element.type === "argument") {
3147
+ const varName = element.value;
3148
+ const varMeta = data._meta?.variables?.[varName];
3149
+ text += varMeta?.format || "%@";
3150
+ }
3151
+ }
3152
+ let xcstringsFormName = form;
3153
+ if (form.startsWith("=")) {
3154
+ const numValue = parseInt(form.substring(1), 10);
3155
+ xcstringsFormName = NUMBER_TO_CLDR_CATEGORY[numValue] || form;
3156
+ }
3157
+ forms[xcstringsFormName] = text;
3158
+ }
3159
+ return forms;
3160
+ }
3161
+ function parseICU(icu) {
3162
+ const match2 = icu.match(/\{(\w+),\s*plural,\s*(.+)\}$/);
3163
+ if (!match2) {
3164
+ throw new Error("Invalid ICU plural format");
3165
+ }
3166
+ const varName = match2[1];
3167
+ const formsText = match2[2];
3168
+ const options = {};
3169
+ let i = 0;
3170
+ while (i < formsText.length) {
3171
+ while (i < formsText.length && /\s/.test(formsText[i])) {
3172
+ i++;
3173
+ }
3174
+ if (i >= formsText.length) break;
3175
+ let formName = "";
3176
+ if (formsText[i] === "=") {
3177
+ formName += formsText[i];
3178
+ i++;
3179
+ while (i < formsText.length && /\d/.test(formsText[i])) {
3180
+ formName += formsText[i];
3181
+ i++;
3182
+ }
3183
+ } else {
3184
+ while (i < formsText.length && /\w/.test(formsText[i])) {
3185
+ formName += formsText[i];
3186
+ i++;
3187
+ }
3188
+ }
3189
+ if (!formName) break;
3190
+ while (i < formsText.length && /\s/.test(formsText[i])) {
3191
+ i++;
3192
+ }
3193
+ if (i >= formsText.length || formsText[i] !== "{") {
3194
+ throw new Error(`Expected '{' after form name '${formName}'`);
3195
+ }
3196
+ i++;
3197
+ let braceCount = 1;
3198
+ let formText = "";
3199
+ while (i < formsText.length && braceCount > 0) {
3200
+ if (formsText[i] === "{") {
3201
+ braceCount++;
3202
+ formText += formsText[i];
3203
+ } else if (formsText[i] === "}") {
3204
+ braceCount--;
3205
+ if (braceCount > 0) {
3206
+ formText += formsText[i];
3207
+ }
3208
+ } else {
3209
+ formText += formsText[i];
3210
+ }
3211
+ i++;
3212
+ }
3213
+ if (braceCount !== 0) {
3214
+ const preview = formsText.substring(
3215
+ Math.max(0, i - 50),
3216
+ Math.min(formsText.length, i + 50)
3217
+ );
3218
+ throw new Error(
3219
+ `Unclosed brace for form '${formName}' in ICU MessageFormat.
3220
+ Expected ${braceCount} more closing brace(s).
3221
+ Context: ...${preview}...
3222
+ Full ICU: {${varName}, plural, ${formsText}}`
3223
+ );
3224
+ }
3225
+ const elements = parseFormText(formText);
3226
+ options[formName] = {
3227
+ value: elements
3228
+ };
3229
+ }
3230
+ return [
3231
+ {
3232
+ type: "plural",
3233
+ value: varName,
3234
+ options
3235
+ }
3236
+ ];
3237
+ }
3238
+ function parseFormText(text) {
3239
+ const elements = [];
3240
+ let currentText = "";
3241
+ let i = 0;
3242
+ while (i < text.length) {
3243
+ if (text[i] === "#") {
3244
+ if (currentText) {
3245
+ elements.push({ type: "literal", value: currentText });
3246
+ currentText = "";
3247
+ }
3248
+ elements.push({ type: "pound" });
3249
+ i++;
3250
+ } else if (text[i] === "{") {
3251
+ if (currentText) {
3252
+ elements.push({ type: "literal", value: currentText });
3253
+ currentText = "";
3254
+ }
3255
+ let braceCount = 1;
3256
+ let j = i + 1;
3257
+ while (j < text.length && braceCount > 0) {
3258
+ if (text[j] === "{") {
3259
+ braceCount++;
3260
+ } else if (text[j] === "}") {
3261
+ braceCount--;
3262
+ }
3263
+ j++;
3264
+ }
3265
+ if (braceCount !== 0) {
3266
+ throw new Error("Unclosed variable reference");
3267
+ }
3268
+ const varName = text.slice(i + 1, j - 1);
3269
+ elements.push({ type: "argument", value: varName });
3270
+ i = j;
3271
+ } else {
3272
+ currentText += text[i];
3273
+ i++;
3274
+ }
3275
+ }
3276
+ if (currentText) {
3277
+ elements.push({ type: "literal", value: currentText });
3278
+ }
3279
+ return elements;
3280
+ }
3281
+
3282
+ // src/cli/loaders/xcode-xcstrings-v2-loader.ts
3283
+ function createXcodeXcstringsV2Loader(defaultLocale = "en") {
3284
+ return createLoader({
3285
+ async pull(locale, input2) {
3286
+ const result = {};
3287
+ for (const [key, value] of Object.entries(input2)) {
3288
+ if (isPluralFormsObject(value)) {
3289
+ try {
3290
+ result[key] = xcstringsToPluralWithMeta(value, locale);
3291
+ } catch (error) {
3292
+ console.error(
3293
+ `
3294
+ [xcode-xcstrings-icu] Failed to convert plural forms for key "${key}":`,
3295
+ `
3296
+ Error: ${error instanceof Error ? error.message : String(error)}`,
3297
+ `
3298
+ Locale: ${locale}
3299
+ `
3300
+ );
3301
+ result[key] = value;
3302
+ }
3303
+ } else {
3304
+ result[key] = value;
3305
+ }
3306
+ }
3307
+ return result;
3308
+ },
3309
+ async push(locale, payload) {
3310
+ const result = {};
3311
+ for (const [key, value] of Object.entries(payload)) {
3312
+ if (isICUPluralObject(value)) {
3313
+ try {
3314
+ const pluralForms = pluralWithMetaToXcstrings(value);
3315
+ result[key] = pluralForms;
3316
+ } catch (error) {
3317
+ throw new Error(
3318
+ `Failed to write plural translation for key "${key}" (locale: ${locale}).
3319
+ ${error instanceof Error ? error.message : String(error)}`
3320
+ );
3321
+ }
3322
+ } else {
3323
+ result[key] = value;
3324
+ }
3325
+ }
3326
+ return result;
3327
+ }
3328
+ });
3329
+ }
3330
+
2944
3331
  // src/cli/loaders/unlocalizable.ts
2945
3332
  import _10 from "lodash";
2946
3333
  import _isUrl from "is-url";
@@ -4544,6 +4931,15 @@ function variableExtractLoader(params) {
4544
4931
  const inputValues = _16.omitBy(input2, _16.isEmpty);
4545
4932
  for (const [key, value] of Object.entries(inputValues)) {
4546
4933
  const originalValue = originalInput[key];
4934
+ if (isICUPluralObject(originalValue)) {
4935
+ const icuValue = isICUPluralObject(value) ? { icu: value.icu } : value;
4936
+ result[key] = {
4937
+ value: icuValue,
4938
+ variables: []
4939
+ // Metadata stored separately, not in variables
4940
+ };
4941
+ continue;
4942
+ }
4547
4943
  const matches = originalValue.match(specifierPattern) || [];
4548
4944
  result[key] = result[key] || {
4549
4945
  value,
@@ -4563,11 +4959,21 @@ function variableExtractLoader(params) {
4563
4959
  const result = {};
4564
4960
  for (const [key, valueObj] of Object.entries(data)) {
4565
4961
  result[key] = valueObj.value;
4962
+ const resultValue = result[key];
4963
+ if (isICUPluralObject(resultValue)) {
4964
+ const originalValue = originalInput?.[key];
4965
+ if (isICUPluralObject(originalValue) && originalValue._meta) {
4966
+ resultValue._meta = originalValue._meta;
4967
+ resultValue[Symbol.for("@lingo.dev/icu-plural-object")] = true;
4968
+ }
4969
+ }
4566
4970
  for (let i = 0; i < valueObj.variables.length; i++) {
4567
4971
  const variable = valueObj.variables[i];
4568
4972
  const currentValue = result[key];
4569
- const newValue = currentValue?.replace(`{variable:${i}}`, variable);
4570
- result[key] = newValue;
4973
+ if (typeof currentValue === "string") {
4974
+ const newValue = currentValue?.replace(`{variable:${i}}`, variable);
4975
+ result[key] = newValue;
4976
+ }
4571
4977
  }
4572
4978
  }
4573
4979
  return result;
@@ -7358,6 +7764,20 @@ function createBucketLoader(bucketType, bucketPathPattern, options, lockedKeys,
7358
7764
  createVariableLoader({ type: "ieee" }),
7359
7765
  createUnlocalizableLoader(options.returnUnlocalizedKeys)
7360
7766
  );
7767
+ case "xcode-xcstrings-v2":
7768
+ return composeLoaders(
7769
+ createTextFileLoader(bucketPathPattern),
7770
+ createPlutilJsonTextLoader(),
7771
+ createJsonLoader(),
7772
+ createXcodeXcstringsLoader(options.defaultLocale),
7773
+ createXcodeXcstringsV2Loader(options.defaultLocale),
7774
+ createFlatLoader({ shouldPreserveObject: isICUPluralObject }),
7775
+ createEnsureKeyOrderLoader(),
7776
+ createLockedKeysLoader(lockedKeys || []),
7777
+ createSyncLoader(),
7778
+ createVariableLoader({ type: "ieee" }),
7779
+ createUnlocalizableLoader(options.returnUnlocalizedKeys)
7780
+ );
7361
7781
  case "yaml":
7362
7782
  return composeLoaders(
7363
7783
  createTextFileLoader(bucketPathPattern),
@@ -11783,7 +12203,7 @@ async function renderHero2() {
11783
12203
  // package.json
11784
12204
  var package_default = {
11785
12205
  name: "lingo.dev",
11786
- version: "0.113.3",
12206
+ version: "0.113.4",
11787
12207
  description: "Lingo.dev CLI",
11788
12208
  private: false,
11789
12209
  publishConfig: {